Upgrade to welshman 0.12.0 and publish through Publisher, renaming thunk code and components to publications
This commit is contained in:
parent
4d4e736c5e
commit
c7669e9cfc
59 changed files with 335 additions and 319 deletions
|
|
@ -86,7 +86,7 @@ imports. `flotilla-views` covers component conventions.
|
|||
- `sync.ts`: `syncApplicationData`, the background sync of user data, spaces and DMs
|
||||
- `settings.ts`: the `Settings` plugin over encrypted app data, plus notification settings
|
||||
- `repository.ts`: the `LatestEvents` plugin, each watched author's most recent event
|
||||
- `thunks.ts` (publish status by event id), `signer.ts` (signer request tracking and `signerHealth`)
|
||||
- `publications.ts` (publish status by event id), `signer.ts` (signer request tracking and `signerHealth`)
|
||||
- `env.ts`: every `VITE_` value, parsed
|
||||
- `logger.ts` (log capture and sending), `analytics.ts` (Plausible pageviews), `device.ts` (a
|
||||
device id)
|
||||
|
|
@ -350,7 +350,7 @@ renders `ArticleForm`.
|
|||
- `flotilla-views`: routes and layouts, components, modals, loading data from components
|
||||
- `flotilla-model`: spaces as relays, NIP-29 rooms, NIP-86 management, content kinds, routing
|
||||
- `welshman`: overview of the packages
|
||||
- `welshman-app`: `App`, `use()`, `AppPolicy`, `DerivedPlugin`, commands and thunks
|
||||
- `welshman-app`: `App`, `use()`, `AppPolicy`, `DerivedPlugin`, commands and the `Publisher`
|
||||
- `welshman-domain`: readers, writers, and adding a kind
|
||||
- `welshman-net`: the pool, sockets and socket policies
|
||||
- `welshman-util`: kind constants, tag specs, `RelaySelection`
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ adds `setRoom` only when it is posting into a room, as `ThreadCreate`, `Classifi
|
|||
Some routing is built into welshman. `Reactions.react` and `Deletes.deleteEvent` find the target's
|
||||
relay in the tracker and copy its `h`. The 10009 writer publishes to the user's outbox and to every
|
||||
space it lists or used to list, so each relay hears about joins and leaves. Kind 9 has no factory,
|
||||
so `RoomChat` and `publishRoomQuote` publish raw templates through `Thunks`.
|
||||
so `RoomChat` and `publishRoomQuote` publish raw templates through `Publisher`.
|
||||
|
||||
Reads are scoped the same way. Request from the space relay (`relays: [url]`, plus `"#h": [h]` for
|
||||
a room) and read results with `$events.forUrl(url, filters)`, which uses the tracker to keep
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: flotilla-state
|
||||
description: "Use this skill when deciding where a piece of state belongs in Flotilla, or when touching state: reading or adding stores in src/app, reaching the App instance and welshman plugins (usePlugin, fromApp, deriveUserItem), writing code that must survive login swapping the app or run signed out, adding an app policy or a flotilla plugin, persisting data (IndexedDB storage, kv/ss, synced stores, published settings, drafts), changing what src/app/sync.ts pulls in the background, and publishing (domain writer → Command → thunk, optimistic updates, undo, showing publish status)."
|
||||
description: "Use this skill when deciding where a piece of state belongs in Flotilla, or when touching state: reading or adding stores in src/app, reaching the App instance and welshman plugins (usePlugin, fromApp, deriveUserItem), writing code that must survive login swapping the app or run signed out, adding an app policy or a flotilla plugin, persisting data (IndexedDB storage, kv/ss, synced stores, published settings, drafts), changing what src/app/sync.ts pulls in the background, and publishing (domain writer → Command → publication, optimistic updates, undo, showing publish status)."
|
||||
---
|
||||
|
||||
# Flotilla state
|
||||
|
|
@ -8,7 +8,7 @@ description: "Use this skill when deciding where a piece of state belongs in Flo
|
|||
State in Flotilla flows one way. Events arrive from relays, pass the ingest policy, and land in
|
||||
the current app's repository. Plugin indexes and derived stores read the repository, and
|
||||
components subscribe to those. Writes go the other way: a domain writer becomes a `Command`, the
|
||||
command becomes a thunk, and the thunk writes its event into the repository before any relay has
|
||||
command becomes a publication, and the `Publisher` writes its event into the repository before any relay has
|
||||
seen it.
|
||||
|
||||
Almost everything per-identity hangs off one welshman `App`, and signing in replaces that app.
|
||||
|
|
@ -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`, `events`, `thunks`, … | `usePlugin` for 28 welshman plugins | safe |
|
||||
| `profiles`, `rooms`, `relays`, `events`, `publisher`, … | `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 |
|
||||
|
|
@ -177,8 +177,8 @@ app, and `app.get()` recurses into building another one.
|
|||
## The repository and derived state
|
||||
|
||||
Events enter `app.repository` from `ingestPolicy` (which calls `tracker.track`, then
|
||||
`repository.publish`), from `Storage` loading the cache at startup, and from thunks publishing
|
||||
optimistically. The tracker records which relays each event was seen on, which is what lets
|
||||
`repository.publish`), from `Storage` loading the cache at startup, and from the `Publisher`
|
||||
publishing optimistically. The tracker records which relays each event was seen on, which is what lets
|
||||
space content be keyed by relay.
|
||||
|
||||
Raw event queries go through welshman's `Events` plugin, exported from `core.ts` as `events`.
|
||||
|
|
@ -233,8 +233,8 @@ A derivation that every row subscribes to, or that joins large sets, is built by
|
|||
- `Chats` (`chats.ts`) updates its `index` incrementally from repository `update` events rather
|
||||
than re-querying. Its `search` reads profile names when it is rebuilt, so it also follows the
|
||||
profiles index.
|
||||
- `thunksByEventId` (`thunks.ts`) indexes thunk history once, and hands back the previous array
|
||||
wherever an event's thunks are unchanged so rows don't churn.
|
||||
- `publicationsByEventId` (`publications.ts`) indexes publish history once, and hands back the
|
||||
previous array wherever an event's publications are unchanged so rows don't churn.
|
||||
- `latestActivityByPath` (`notifications.ts`) joins chats, room lists, relay info, events and
|
||||
settings behind `throttled(1000, …)`.
|
||||
- `LatestEvents.forPubkey` (`repository.ts`) shares one repository listener across every
|
||||
|
|
@ -357,7 +357,7 @@ components; see flotilla-views.
|
|||
|
||||
## Mutations
|
||||
|
||||
The prevailing path runs from a domain writer to a command to a thunk, adapted from
|
||||
The prevailing path runs from a domain writer to a command to a publication, adapted from
|
||||
`ThreadCreate.svelte`:
|
||||
|
||||
```typescript
|
||||
|
|
@ -371,8 +371,8 @@ if (room) {
|
|||
eventWriter.setRoom(url, room)
|
||||
}
|
||||
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(eventWriter).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
|
|
@ -389,29 +389,30 @@ Plugin mutators already return a `Command`: `roomLists.get().addRelay(url).then(
|
|||
`forceLoad` before writing. A replaceable event you build yourself needs the same, as in
|
||||
`publishSettings`.
|
||||
|
||||
Some call sites call `thunks.get().publish({event, relays, delay})` directly. Anything that
|
||||
Some call sites call `publisher.get().publish({event, relays, delay})` directly. Anything that
|
||||
honours the `send_delay` window does, because `Command` cannot carry it: room chat
|
||||
(`RoomChat.svelte`), and `publishComment` (behind both comment composers) and `publishRoomQuote` in
|
||||
`rooms.ts`. So do the push adapters and `ProfileDelete.svelte`. DMs go through
|
||||
`wraps.get().publish({event, recipients})`, which returns a merged thunk (see `reactions.ts`).
|
||||
NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return
|
||||
`wraps.get().publish({event, recipients})`, which returns a `PublicationGroup` (see `reactions.ts`).
|
||||
NIP-86 calls (`relayManagement.get().forUrl(url)`) don't go through the `Publisher`. They return
|
||||
`{result, error}`, and the caller handles `error`.
|
||||
|
||||
### Optimistic updates, undo and status
|
||||
|
||||
- **Optimistic writes.** `Thunks` writes the event into the repository and tracks it against its
|
||||
- **Optimistic writes.** `Publisher` writes the event into the repository and tracks it against its
|
||||
relays when it is enqueued, so every derived store sees it immediately. Signing then swaps the
|
||||
unsigned event for the signed one.
|
||||
- **Undo.** `thunk.abort()` during the `delay` removes the event from the repository and from
|
||||
`history`. When `send_delay` is set, room chat shows a `ThunkToast` whose Cancel button aborts,
|
||||
and a comment carries the same Cancel in the `ThunkPending` row under it.
|
||||
unsigned event for the signed one, and `publication.event` follows it.
|
||||
- **Undo.** `publication.abort()` while `canAbort()` holds (nothing has reached a relay yet) removes
|
||||
the event from the repository and from `history`. When `send_delay` is set, room chat shows a
|
||||
`PublicationToast` whose Cancel button aborts, and a comment carries the same Cancel in the
|
||||
`PublicationPending` row under it.
|
||||
- **Editing.** Editing a message deletes it and republishes with the same `created_at` (see
|
||||
`RoomChat.svelte`).
|
||||
- **Status in rows.** Rows look up `$thunksByEventId.get(event.id) ?? noThunks` and pass
|
||||
`$thunks.merge(...)` to `ThunkStatus`, or to `ThunkFailure`, which retries per relay.
|
||||
`ThunkStatusOrDeleted` combines publish status with deletion. `ChatMessage.svelte` filters the
|
||||
whole `history` per row instead.
|
||||
- **Status in forms.** Forms await `waitForError()` and toast the message, as in the excerpt
|
||||
- **Status in rows.** Rows look up `$publicationsByEventId.get(event.id) ?? noPublications` and
|
||||
pass `$publisher.merge(...)` to `PublicationStatus`, or to `PublicationFailure`, which retries per
|
||||
relay. A retry drops the publications it replaces from `history`, so the row shows the retry's
|
||||
outcome. `PublicationStatusOrDeleted` combines publish status with deletion.
|
||||
- **Status in forms.** Forms await `settled()`, read `getError()` and toast the message, as in the excerpt
|
||||
above.
|
||||
|
||||
## Other app-level stores
|
||||
|
|
@ -466,7 +467,7 @@ Take the first answer that fits:
|
|||
- `flotilla-architecture`: the layer rules, what each `src/app` module is for, boot at a glance
|
||||
- `flotilla-views`: routes, components, and how components load data and show state
|
||||
- `flotilla-model`: spaces, rooms, NIP-43/29/86, which relays events go to, domain kinds
|
||||
- `welshman-app`: `App`, plugins, `Command`, thunks, `Network`/`Sync`, `Events`
|
||||
- `welshman-app`: `App`, plugins, `Command`, `Publisher`, `Network`/`Sync`, `Events`
|
||||
- `welshman-store`: `deriveEventsById`, `deriveItemsByKey`, `synced`, `throttled`, `withGetter`
|
||||
- `welshman-domain`: the readers and writers behind `reader`, `writer` and `command`
|
||||
- `welshman-net`: the repository, tracker and socket policies under the app
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ Svelte's delegated `onclick` on the `<ul>` would never see the click. On mobile,
|
|||
```typescript
|
||||
pushToast({message: "Role created!"})
|
||||
pushToast({theme: "error", message, action: {message: "Details", onclick}})
|
||||
pushToast({timeout: 30_000, children: {component: ThunkToast, props: {thunk}}})
|
||||
pushToast({timeout: 30_000, children: {component: PublicationToast, props: {publication}}})
|
||||
```
|
||||
|
||||
A `children` component receives the `toast` as a prop, so it can pop itself. `clip(value)` copies
|
||||
|
|
@ -293,7 +293,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`, `$events`, `$network`, `$thunks`, `$deletes`,
|
||||
(`$profiles`, `$relays`, `$rooms`, `$roomLists`, `$events`, `$network`, `$publisher`, `$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.
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ ahead. All ten `*Item` components that appear in a list use it.
|
|||
|
||||
## Mutations from a component
|
||||
|
||||
flotilla-state covers the writer → command → thunk path. The component around it owns a
|
||||
flotilla-state covers the writer → command → publication path. The component around it owns a
|
||||
`loading` flag, awaits the first error, toasts it, and closes. Anything thrown on the way (a signer
|
||||
refusing, a relay unreachable) is logged and toasted in a `catch`, so a bare `try`/`finally` never
|
||||
lets it escape the handler:
|
||||
|
|
@ -408,8 +408,8 @@ const submit = async () => {
|
|||
loading = true
|
||||
|
||||
try {
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(eventWriter).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
|
|
@ -428,13 +428,13 @@ const submit = async () => {
|
|||
Bind the flag to the button with `disabled={loading}` and `<Spinner {loading}>`. Plugin mutators
|
||||
return a `Command` as well: `$deletes.deleteEvent(event, w => w.setProtected(protect))` then
|
||||
`.publishToRelays([url])` in `EventDeleteConfirm`, or `$rooms.createRoom(url, room)` then
|
||||
`.publish()` in `RoomForm`. NIP-86 calls resolve to `{error}` instead of a thunk:
|
||||
`.publish()` in `RoomForm`. NIP-86 calls resolve to `{error}` instead of a publication:
|
||||
`$relayManagement.forUrl(url).createRole(...)` in `RoleCreate`.
|
||||
|
||||
The hosting backend isn't nostr. Toast `hostingErrorMessage(e, fallback)` from `hosting.ts`,
|
||||
which returns a `HostingError`'s own message and logs anything else before falling back. Publishes are
|
||||
optimistic, so rows show progress in place: `ContentActions`, `CommentActions` and `ArticleActionBar`
|
||||
wrap their contents in `ThunkStatusOrDeleted`, and chat pushes a `ThunkToast`.
|
||||
wrap their contents in `PublicationStatusOrDeleted`, and chat pushes a `PublicationToast`.
|
||||
|
||||
## Forms
|
||||
|
||||
|
|
@ -481,9 +481,9 @@ Rich text uses `makeEditor` from `src/app/editor` with `EditorContent`.
|
|||
## Related skills
|
||||
|
||||
- `flotilla-architecture`: layer rules and exceptions, the `src/app` modules, boot, placement
|
||||
- `flotilla-state`: plugin stores, `derive*` stores, drafts, settings, thunks and commands
|
||||
- `flotilla-state`: plugin stores, `derive*` stores, drafts, settings, publications and commands
|
||||
- `flotilla-model`: spaces, rooms, NIP-43/29/86, content kinds, and where events are published
|
||||
- `welshman-app`: plugins, projections, `Command`, thunks, `Events`, `Feeds`
|
||||
- `welshman-app`: plugins, projections, `Command`, `Publisher`, `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`
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ PascalCase. This is what the qualifiers mean, drawn from the ~320 components the
|
|||
| `Detail` | a modal with everything known about an entity | `ProfileDetail`, `RoomDetail`, `ContentLinkDetail` |
|
||||
| `Info` | a modal with an event's underlying data | `EventInfo`, `ProfileInfo` |
|
||||
| `Summary` | a compact read-only digest, inline | `RelaySummary`, `GoalSummary`, `ReactionSummary` |
|
||||
| `Status` | a small badge or indicator | `ClassifiedStatus`, `SignerStatus`, `ThunkStatus` |
|
||||
| `Status` | a small badge or indicator | `ClassifiedStatus`, `SignerStatus`, `PublicationStatus` |
|
||||
| `Card` | a framed presentation of an event | `NoteCard`, `HomeInboxItemCard` |
|
||||
| `Bar` | a horizontal strip across the top or bottom of something | `SpaceBar`, `ComposeBar`, `EventActionBar` |
|
||||
| `Compose` | a message composer | `ChatCompose`, `RoomCompose`, `CommentCompose` |
|
||||
|
|
@ -41,7 +41,7 @@ per content kind). Four are families instead:
|
|||
- `Note*` renders an event as a note: `NoteItem`, `NoteCard`, `NoteContent`, one
|
||||
`NoteContent<Kind>` per kind, and a compact `NoteContentMinimal<Kind>`.
|
||||
- `Content*` renders parsed content tokens: `ContentMention`, `ContentQuote`, `ContentTopic`.
|
||||
- `Home*` are the dashboard's sections; `LogIn*` and `SignUp*` are auth steps; `Thunk*` show
|
||||
- `Home*` are the dashboard's sections; `LogIn*` and `SignUp*` are auth steps; `Publication*` show
|
||||
publish status; `Info*` are explainer dialogs (`InfoNostr`, `InfoKeys`, `InfoRelay`).
|
||||
|
||||
Eleven components are a bare entity, being the root of their feature: `Chat`, `Content`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: welshman-app
|
||||
description: "Use this skill when working with @welshman/app: the instance-based client for building nostr applications — creating an App instance, the use() plugin registry, User & sessions, reactive data stores (profiles, follows, mutes, relay lists, handles, zappers), optimistic publishing with thunks, outbox-model requests, routing, web of trust, feeds, and search."
|
||||
description: "Use this skill when working with @welshman/app: the instance-based client for building nostr applications — creating an App instance, the use() plugin registry, User & sessions, reactive data stores (profiles, follows, mutes, relay lists, handles, zappers), optimistic publishing through a staged pipeline, outbox-model requests, routing, web of trust, feeds, and search."
|
||||
---
|
||||
|
||||
# welshman/app — Instance-Based Nostr App
|
||||
|
|
@ -26,7 +26,7 @@ Peer deps: `svelte` (4 or 5), all `@welshman/*` workspace packages, and `@pomade
|
|||
2. **Features are plugins**, resolved lazily and memoized via `app.use(SomeClass)`. Each plugin is constructed with the app and cached per app.
|
||||
3. **`Projection<T>` is the universal accessor.** It has `.get()` (sync snapshot) and `.$` (Svelte `Readable`). Bind `.$` in components; call `.get()` in callbacks/hot paths.
|
||||
4. **Reads are reactive and lazy-loading.** `app.use(Profiles).one(pubkey)` returns a store that fetches over the network (outbox model) and updates as events arrive.
|
||||
5. **Writes are optimistic.** Publishing goes through *thunks*: the event hits the local repository immediately, signs lazily, and reports per-relay progress, with an abortable delay for soft-undo.
|
||||
5. **Writes are optimistic.** Publishing goes through the `Publisher` pipeline: the event hits the local repository immediately, signs lazily, and reports per-relay progress, with an abortable delay for soft-undo.
|
||||
|
||||
## Creating an app
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ setNip55Plugin(NostrSignerPlugin)
|
|||
|
||||
All follow the same shape — `get(key)` (sync), `one(key)` (reactive, lazy-loads), `load(key)`/`forceLoad(key)` (promises), plus convenience accessors returning `Projection`. Resolve with `app.use(...)`.
|
||||
|
||||
Every mutation method (`create`/`update`/`follow`/`addRelay`/`setRelays`/etc.) is `async` and returns a **`Command`**, not a `Thunk` — it builds the event but does not publish it. Call `.publish()` on the result to actually send it. See [Commands](#commands-deferred-publishing) below.
|
||||
Every mutation method (`create`/`update`/`follow`/`addRelay`/`setRelays`/etc.) is `async` and returns a **`Command`**, not a `Publication` — it builds the event but does not publish it. Call `.publish()` on the result to actually send it. See [Commands](#commands-deferred-publishing) below.
|
||||
|
||||
| Plugin | Data | Notable accessors |
|
||||
|---|---|---|
|
||||
|
|
@ -144,7 +144,7 @@ const writeRelays = app.use(RelayLists).writeUrls(pubkey).get() // string[]
|
|||
|
||||
// Mutations return a Command — build it, then decide how to publish it
|
||||
const command = await app.use(RelayLists).addWriteUrl("wss://relay.example")
|
||||
command.publish() // normal outbox/relays flow via Thunks
|
||||
command.publish() // normal outbox/relays flow via Publisher
|
||||
// or: command.publishToRelays(["wss://relay.example"]) // send straight to one relay
|
||||
|
||||
// Since these methods are async, `publish`/`publishToRelays` free functions avoid a double-await:
|
||||
|
|
@ -152,42 +152,46 @@ import {publish} from "@welshman/app"
|
|||
await app.use(RelayLists).addWriteUrl("wss://relay.example").then(publish)
|
||||
```
|
||||
|
||||
## Publishing (optimistic thunks)
|
||||
## Publishing (optimistic pipeline)
|
||||
|
||||
```typescript
|
||||
import {Thunks, Router} from "@welshman/app"
|
||||
import {Publisher, Router} from "@welshman/app"
|
||||
import {makeEvent, NOTE, userOutbox} from "@welshman/util"
|
||||
|
||||
// There's no dedicated outbox helper on Thunks — resolve write relays yourself via the
|
||||
// Router's Resolver + the RelaySelection DSL (this is what Command.publish() does under the
|
||||
// hood for every data-plugin mutation, whose `relays` come from the writer's own routes):
|
||||
const thunk = app.use(Thunks).publish({
|
||||
// Publisher has no outbox helper. Resolve write relays with the Router's Resolver and the
|
||||
// RelaySelection DSL, as Command.publish() does for every data-plugin mutation:
|
||||
const publication = app.use(Publisher).publish({
|
||||
event: makeEvent(NOTE, {content: "hi"}),
|
||||
relays: await app.use(Router).resolver.relays([userOutbox()]), // Promise<string[]>
|
||||
delay: 3000, // abortable soft-undo window (ms)
|
||||
delay: 3000, // abortable soft-undo window (ms), before signing
|
||||
})
|
||||
|
||||
// To specific relays:
|
||||
app.use(Thunks).publish({event, relays: ["wss://relay.example"]})
|
||||
app.use(Publisher).publish({event, relays: ["wss://relay.example"]})
|
||||
|
||||
// A thunk is a Svelte store with per-relay status:
|
||||
thunk.subscribe(t => console.log(t.results))
|
||||
thunk.abort() // effective only before `delay` elapses
|
||||
await thunk.waitForCompletion()
|
||||
thunk.getError() // string | undefined
|
||||
app.use(Thunks).history // writable<Thunk[]> — optimistic log
|
||||
app.use(Thunks).retry(thunk)
|
||||
app.use(Thunks).retry(thunk, [url]) // only to these relays; a merged thunk retries only children that targeted them
|
||||
// A Publication is a Svelte store. `state` is a union keyed by `stage`:
|
||||
// queued → delayed → wrapping → pow → signing → sending → sent | failed | aborted
|
||||
publication.subscribe(p => console.log(p.state.stage, p.results))
|
||||
publication.abort() // unwinds the optimistic write; does nothing once settled
|
||||
publication.canAbort() // true until any relay has been sent to
|
||||
await publication.settled() // resolves on a terminal stage
|
||||
publication.getError() // string | undefined
|
||||
publication.event // current event (mined/signed as it goes); the rumor for a wrap
|
||||
app.use(Publisher).history // writable<Publication[]> — optimistic log
|
||||
app.use(Publisher).retry(publication) // re-sends publication.event, same id
|
||||
app.use(Publisher).retry(publication, [url]) // only to these relays; a group retries only members that targeted them
|
||||
// retry drops the publications it replaces from `history`
|
||||
app.use(Publisher).merge([a, b]) // PublicationGroup; a relay's status is the worst among members
|
||||
|
||||
// Gift-wrapped (NIP-59): single recipient via `recipient`, or many via Wraps:
|
||||
app.use(Thunks).publish({event, relays, recipient: theirPubkey})
|
||||
const merged = await app.use(Wraps).publish({event: rumor, recipients: [a, b]})
|
||||
app.use(Publisher).publish({event, relays, recipient: theirPubkey})
|
||||
const group = await app.use(Wraps).publish({event: rumor, recipients: [a, b]})
|
||||
|
||||
// Proof of work (NIP-13):
|
||||
app.use(Thunks).publish({event, relays, pow: 20})
|
||||
app.use(Publisher).publish({event, relays, pow: 20})
|
||||
```
|
||||
|
||||
`ThunkOptions`: `{event, relays?, recipient?, delay?, pow?, ...PublishOptions}` (`app` is injected). Incoming wraps addressed to the user are auto-unwrapped by the default `appPolicyWraps`. An unwrap that throws is tried again up to `MAX_UNWRAP_ATTEMPTS` times, so a signer that was busy or disconnected doesn't cost the message.
|
||||
`PublishRequest`: `{event, relays, recipient?, delay?, pow?, priority?, timeout?}`. The stages (`STAGES` in `plugins/publisher.ts`) only transform the event. The publisher keeps the repository, tracker and `WrapManager` in step by reacting to state changes. Incoming wraps addressed to the user are auto-unwrapped by the default `appPolicyWraps`. An unwrap that throws is tried again up to `MAX_UNWRAP_ATTEMPTS` times, so a signer that was busy or disconnected doesn't cost the message.
|
||||
|
||||
## Commands (deferred publishing)
|
||||
|
||||
|
|
@ -202,11 +206,11 @@ command.app // the IApp it was built for
|
|||
command.event // EventTemplate — unsigned, inspectable before publishing
|
||||
command.relays // string[] — where publish() will send it
|
||||
|
||||
command.publish() // normal path: app.use(Thunks).publish({event, relays: command.relays})
|
||||
command.publish() // normal path: app.use(Publisher).publish({event, relays: command.relays})
|
||||
command.publishToRelays(urls) // publish to a specific relay set instead of command.relays
|
||||
```
|
||||
|
||||
This lets a caller preview/log a command, choose a different transport, or drop it entirely, instead of every plugin method publishing unconditionally. `Wraps.publish` is the one exception — it fans a single rumor out to a `MergedThunk` of per-recipient wraps (each with its own relays), which doesn't fit the one-event/one-relay-set `Command` shape, so it still publishes directly.
|
||||
This lets a caller preview/log a command, choose a different transport, or drop it entirely, instead of every plugin method publishing unconditionally. `Wraps.publish` is the one exception — it fans a single rumor out to a `PublicationGroup` of per-recipient wraps (each with its own relays), which doesn't fit the one-event/one-relay-set `Command` shape, so it still publishes directly.
|
||||
|
||||
`publish`/`publishToRelays` are also exported as free functions (e.g. `(command) => command.publish()`, `(urls) => (command) => command.publishToRelays(urls)`) so you can chain straight off the mutation method's promise instead of double-awaiting:
|
||||
|
||||
|
|
@ -441,7 +445,7 @@ const app = new App({user, policies: [appPolicyIngest, appPolicyAuthNever]})
|
|||
| `addSession(...)` / `pubkey.get()` | `User.fromSession(...)` + `createApp({user})`; `app.user?.pubkey` |
|
||||
| `deriveProfile(pk)` | `app.use(Profiles).one(pk)` |
|
||||
| `deriveProfileDisplay(pk)` | `app.use(Profiles).display(pk).$` |
|
||||
| `publishThunk({...})` | `app.use(Thunks).publish({...})` (resolve outbox relays via `await app.use(Router).resolver.relays([userOutbox()])`) |
|
||||
| `publishThunk({...})` | `app.use(Publisher).publish({...})` (resolve outbox relays via `await app.use(Router).resolver.relays([userOutbox()])`) |
|
||||
| `follow(tag)` / `mute(tag)` | `app.use(FollowLists).follow(tag).then(publish)` / `app.use(MuteLists).mutePublicly(tag).then(publish)`, which return a [`Command`](#commands-deferred-publishing) |
|
||||
| `load({...})` / `request({...})` | `app.use(Network).loadComplete({...})` / `request({...})` |
|
||||
| `Router.get().FromUser()` / `router.Event(e)` | `app.use(Router).resolver` + the `RelaySelection` DSL (`resolver.relays([userOutbox()])`, `resolver.relay([seen(e)])`) |
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ Conventions below.
|
|||
A `Projection<T>` is `{get(): T, $: Readable<T>}` — bind `.$` in markup, call `.get()` in
|
||||
callbacks and hot paths.
|
||||
|
||||
**Thunks:**
|
||||
**Publications** (`app.use(Publisher).publish`):
|
||||
|
||||
- Reduce UI latency by handling signatures and sending in background
|
||||
- Return status that should be displayed to user
|
||||
|
|
@ -93,7 +93,7 @@ callbacks and hot paths.
|
|||
|
||||
**Welshman Library Suite:**
|
||||
|
||||
- `@welshman/app` - The `App` instance and its plugins (Profiles, Rooms, Thunks, Router, …)
|
||||
- `@welshman/app` - The `App` instance and its plugins (Profiles, Rooms, Publisher, Router, …)
|
||||
- `@welshman/domain` - A typed Reader/Writer pair per event kind, plus `Relay` and `Zapper`
|
||||
- `@welshman/net` - Network layer (Pool, Socket, adapters, request/publish/pull)
|
||||
- `@welshman/store` - Svelte integration (deriveEventsById, deriveItemsByKey, etc.)
|
||||
|
|
@ -188,7 +188,7 @@ callbacks and hot paths.
|
|||
1. Build a writer: `app.use(Domain).writer(Kind, reader?)`, then chain its setters
|
||||
2. Wrap it: `const command = await app.use(Domain).command(writer)`
|
||||
3. Publish it: `command.publish()` or `.publishToRelays(urls)`
|
||||
4. Display thunk status to user (for cancel/error handling)
|
||||
4. Display publication status to user (for cancel/error handling)
|
||||
|
||||
Plugin mutators (`app.use(FollowLists).follow(...)`, `app.use(Rooms).joinRoom(...)`, …) already
|
||||
return a `Command`, so `.then(publish)` is usually all you need.
|
||||
|
|
|
|||
|
|
@ -1897,7 +1897,7 @@ does not serve is answered by an empty relay rather than refused, so no spec can
|
|||
that fails. That leaves one invariant untested: a send whose reads fail before the message exists
|
||||
must keep the text in the composer and say so, rather than clearing as though it went. It is the
|
||||
shape of the bug that motivated US-108 — `@welshman/store`'s `load` rejects rather than resolving
|
||||
empty, so a failure there aborts a publish before its thunk is made and nothing reaches the
|
||||
empty, so a failure there aborts a publish before its publication is made and nothing reaches the
|
||||
timeline to carry a status. Testing it needs a seam for making a relay unusable.
|
||||
|
||||
**Internals with no user-visible surface.** The legacy session-storage format
|
||||
|
|
|
|||
20
package.json
20
package.json
|
|
@ -97,16 +97,16 @@
|
|||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"@welshman/app": "^0.11.3",
|
||||
"@welshman/content": "^0.11.3",
|
||||
"@welshman/domain": "^0.11.3",
|
||||
"@welshman/editor": "^0.11.3",
|
||||
"@welshman/feeds": "^0.11.3",
|
||||
"@welshman/lib": "^0.11.3",
|
||||
"@welshman/net": "^0.11.3",
|
||||
"@welshman/signer": "^0.11.3",
|
||||
"@welshman/store": "^0.11.3",
|
||||
"@welshman/util": "^0.11.3",
|
||||
"@welshman/app": "^0.12.0",
|
||||
"@welshman/content": "^0.12.0",
|
||||
"@welshman/domain": "^0.12.0",
|
||||
"@welshman/editor": "^0.12.0",
|
||||
"@welshman/feeds": "^0.12.0",
|
||||
"@welshman/lib": "^0.12.0",
|
||||
"@welshman/net": "^0.12.0",
|
||||
"@welshman/signer": "^0.12.0",
|
||||
"@welshman/store": "^0.12.0",
|
||||
"@welshman/util": "^0.12.0",
|
||||
"cheerio": "^1.2.0",
|
||||
"compressorjs-next": "^1.1.2",
|
||||
"dompurify": "^3.4.13",
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -16,7 +16,7 @@ import {
|
|||
import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
|
||||
import {Sync, User, publish} from "@welshman/app"
|
||||
import {stripPrefix} from "@lib/util"
|
||||
import {app, command, roomLists, thunks, writer} from "@app/core"
|
||||
import {app, command, publisher, roomLists, writer} from "@app/core"
|
||||
import {PLATFORM_URL} from "@app/env"
|
||||
import {relaysMostlyRestricted} from "@app/policies"
|
||||
import {optInSpaceNotifications} from "@app/push"
|
||||
|
|
@ -109,7 +109,7 @@ export const publishLeaveRequest = (url: string) =>
|
|||
|
||||
// "duplicate:" and "already a member" both leave us where we wanted to be.
|
||||
const isMembershipRefusal = (error: string) =>
|
||||
Boolean(error) && !error.startsWith("duplicate:") && !error.includes("already")
|
||||
!error.startsWith("duplicate:") && !error.includes("already")
|
||||
|
||||
// Joining takes two publishes: the NIP-29 request the relay can refuse, and the user's room list.
|
||||
export const joinRoom = async (url: string, h: string, code?: string) => {
|
||||
|
|
@ -119,10 +119,10 @@ export const joinRoom = async (url: string, h: string, code?: string) => {
|
|||
eventWriter.setClaim(code)
|
||||
}
|
||||
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(eventWriter).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (isMembershipRefusal(error)) {
|
||||
if (error && isMembershipRefusal(error)) {
|
||||
return error
|
||||
}
|
||||
|
||||
|
|
@ -130,10 +130,10 @@ export const joinRoom = async (url: string, h: string, code?: string) => {
|
|||
}
|
||||
|
||||
export const leaveRoom = async (url: string, h: string) => {
|
||||
const thunk = await command(writer(RoomLeave).setRoom(url, h)).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(writer(RoomLeave).setRoom(url, h)).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (isMembershipRefusal(error)) {
|
||||
if (error && isMembershipRefusal(error)) {
|
||||
return error
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +155,8 @@ export const publishRoomInvite = async (url: string, h: string) => {
|
|||
],
|
||||
})
|
||||
|
||||
const error = await thunks
|
||||
.get()
|
||||
.publish({event, relays: [url]})
|
||||
.waitForError()
|
||||
const publication = publisher.get().publish({event, relays: [url]})
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return {error, code: undefined}
|
||||
|
|
@ -223,23 +221,21 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||
return formatAuthError(socket.auth.status, socket.auth.details)
|
||||
}
|
||||
|
||||
const thunk = await publishJoinRequest(url, claim)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await publishJoinRequest(url, claim)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (shouldIgnoreError(error)) {
|
||||
return
|
||||
if (error && !shouldIgnoreError(error)) {
|
||||
if (error.includes("invite code")) {
|
||||
return "join request rejected"
|
||||
}
|
||||
|
||||
// A space that isn't open to the public refuses a join carrying no claim at all
|
||||
if (error.includes("claim")) {
|
||||
return "This space requires an invite code"
|
||||
}
|
||||
|
||||
return stripPrefix(error)
|
||||
}
|
||||
|
||||
if (error.includes("invite code")) {
|
||||
return "join request rejected"
|
||||
}
|
||||
|
||||
// A space that isn't open to the public refuses a join carrying no claim at all
|
||||
if (error.includes("claim")) {
|
||||
return "This space requires an invite code"
|
||||
}
|
||||
|
||||
return stripPrefix(error)
|
||||
}
|
||||
|
||||
export const completeSpaceJoin = async (url: string, notifications: boolean) => {
|
||||
|
|
|
|||
|
|
@ -14,7 +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, events, reader, relays, thunks, writer} from "@app/core"
|
||||
import {deletes, events, publisher, reader, relays, writer} from "@app/core"
|
||||
import {addressWriterToSpace} from "@app/rooms"
|
||||
import {kv} from "@app/storage"
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ export const publishRsvp = async (url: string, event: TrustedEvent, status: Rsvp
|
|||
reader(TimeEvent)(event).room(),
|
||||
)
|
||||
|
||||
return thunks.get().publish({relays: [url], event: await eventWriter.renderTemplate()})
|
||||
return publisher.get().publish({relays: [url], event: await eventWriter.renderTemplate()})
|
||||
}
|
||||
|
||||
export const retractRsvp = async (url: string, rsvp: TrustedEvent) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import PublicationStatusOrDeleted from "@app/components/PublicationStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActionBar from "@app/components/EventActionBar.svelte"
|
||||
import {events} from "@app/core"
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
<div data-component="ArticleActionBar" class="flex w-full min-w-0 flex-col gap-3">
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<PublicationStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-top" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</PublicationStatusOrDeleted>
|
||||
{#if !$deleted}
|
||||
<EventActionBar {url} {event} noun="Article">
|
||||
{#snippet leading()}
|
||||
|
|
|
|||
|
|
@ -131,14 +131,14 @@
|
|||
eventWriter.setImage(image)
|
||||
}
|
||||
|
||||
const {thunk, error} = await publishToSpace(eventWriter, {url, h, shareToChat})
|
||||
const {publication, error} = await publishToSpace(eventWriter, {url, h, shareToChat})
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
goto(makeArticlePath(url, getAddress(thunk.options.event)))
|
||||
goto(makeArticlePath(url, getAddress(publication.event)))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
pushToast({theme: "error", message: "Failed to publish your article."})
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
const command = await $deletes.deleteEvent(board.event, writer =>
|
||||
writer.setProtected(protect),
|
||||
)
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
const error = (await command.publishToRelays([url]).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import type {TrustedEvent, EventTemplate} from "@welshman/util"
|
||||
import {makeEvent, userInbox, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
||||
import {parse, isLink} from "@welshman/content"
|
||||
import {MessagingRelayLists, Thunks} from "@welshman/app"
|
||||
import {MessagingRelayLists, Publisher} from "@welshman/app"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
import ChatTitle from "@app/components/ChatTitle.svelte"
|
||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||
import ComposeArea, {type ComposeValues} from "@app/components/ComposeArea.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import PublicationToast from "@app/components/PublicationToast.svelte"
|
||||
import {app, deletes, router, user, wraps} from "@app/core"
|
||||
import {loadSendDelay} from "@app/settings"
|
||||
import {chats, makeChatId} from "@app/chats"
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
const delay = await loadSendDelay()
|
||||
|
||||
// Kind 15 carries one image each per nip 17, and a second's sleep keeps the timestamps distinct.
|
||||
const thunks = await Promise.all(
|
||||
const groups = await Promise.all(
|
||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||
$wraps.publish({
|
||||
event,
|
||||
|
|
@ -134,8 +134,8 @@
|
|||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk: $app.use(Thunks).merge(thunks)},
|
||||
component: PublicationToast,
|
||||
props: {publication: $app.use(Publisher).merge(groups)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
writer.setReadUrls(DEFAULT_RELAYS).setWriteUrls(DEFAULT_RELAYS),
|
||||
)
|
||||
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
}
|
||||
|
||||
const command = await $messagingRelayLists.setUrls(DEFAULT_MESSAGING_RELAYS)
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@
|
|||
import Content from "@app/components/Content.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import PublicationFailure from "@app/components/PublicationFailure.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||
import {profiles, thunks, user} from "@app/core"
|
||||
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||
import {profiles, publisher, user} from "@app/core"
|
||||
import {noPublications, publicationsByEventId} from "@app/publications"
|
||||
import {highlightedEvent} from "@app/routes"
|
||||
import {colorFor} from "@app/theme"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
|
@ -39,7 +39,9 @@
|
|||
|
||||
const isOwn = event.pubkey === $user.pubkey
|
||||
const profileDisplay = $profiles.display(event.pubkey).$
|
||||
const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks))
|
||||
const publication = $derived(
|
||||
$publisher.merge($publicationsByEventId.get(event.id) ?? noPublications),
|
||||
)
|
||||
const colorValue = colorFor(event.pubkey)
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
|
|
@ -58,7 +60,7 @@
|
|||
let tippy: Maybe<TippyController> = $state()
|
||||
</script>
|
||||
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-1" />
|
||||
<PublicationFailure showToastOnRetry {publication} class="mt-1" />
|
||||
<div
|
||||
data-event={event.id}
|
||||
class={cx("group flex items-center justify-end gap-1 px-2", {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import PublicationStatusOrDeleted from "@app/components/PublicationStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<PublicationStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</PublicationStatusOrDeleted>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {thunks, writer} from "@app/core"
|
||||
import {publisher, writer} from "@app/core"
|
||||
import {DraftKey} from "@app/drafts"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {publishComment} from "@app/rooms"
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
return publishComment({url, event, parent, content, tags})
|
||||
}
|
||||
|
||||
return $thunks.publish({
|
||||
return $publisher.publish({
|
||||
...(await writer(Note)
|
||||
.setContent(content)
|
||||
.addTags(...tags)
|
||||
|
|
@ -67,8 +67,8 @@
|
|||
loading = true
|
||||
|
||||
try {
|
||||
const thunk = await publishReply(content, ed.storage.nostr.getEditorTags())
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await publishReply(content, ed.storage.nostr.getEditorTags())
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import PublicationStatusOrDeleted from "@app/components/PublicationStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import {user} from "@app/core"
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
</div>
|
||||
<span class="h-6 w-px shrink-0 bg-line"></span>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event} {context} {status}>
|
||||
<PublicationStatusOrDeleted {event} {context} {status}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</ThunkStatusOrDeleted>
|
||||
</PublicationStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@
|
|||
loading = true
|
||||
|
||||
try {
|
||||
const thunk = await setFeaturedContent(url, content)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await setFeaturedContent(url, content)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
if ($adminDelete === AdminDelete.Room) {
|
||||
const command = await $rooms.deleteEvent(url, {h}, event.id)
|
||||
|
||||
return command.publish().waitForError()
|
||||
return (await command.publish().settled()).getError()
|
||||
}
|
||||
|
||||
const {error} = await $relayManagement.forUrl(url).banEvent(event.id)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
const confirm = async () => {
|
||||
const protect = await shouldProtect
|
||||
const command = await $deletes.deleteEvent(event, writer => writer.setProtected(protect))
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
const error = (await command.publishToRelays([url]).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
parent?: TrustedEvent
|
||||
onClose: () => void
|
||||
onClearParent?: () => void
|
||||
onSubmit: (thunk: unknown) => void
|
||||
onSubmit: (publication: unknown) => void
|
||||
}
|
||||
|
||||
const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props()
|
||||
|
|
@ -54,14 +54,14 @@
|
|||
const tags = ed.storage.nostr.getEditorTags()
|
||||
|
||||
try {
|
||||
const thunk = await publishComment({
|
||||
const publication = await publishComment({
|
||||
url,
|
||||
event,
|
||||
...(await prependParent(parent, {content, tags}, url)),
|
||||
})
|
||||
|
||||
draftKey.clear()
|
||||
onSubmit(thunk)
|
||||
onSubmit(publication)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
pushToast({theme: "error", message: "Failed to send your reply."})
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
const save = async (plan: HealthCheckPlan) => {
|
||||
const command = await plan.apply()
|
||||
const error = command && (await publish(command).waitForError())
|
||||
const error = command && (await publish(command).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: `Your relays couldn't be saved: ${errorMessage(error)}`})
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
try {
|
||||
const protect = await $relays.hasNip(url, 70)
|
||||
const command = await $deletes.deleteEvent(pin.event, writer => writer.setProtected(protect))
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
const error = (await command.publishToRelays([url]).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@
|
|||
.setTopics(topics)
|
||||
.setContent(ed.getText({blockSeparator: "\n"}).trim())
|
||||
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const publication = await command(eventWriter).then(publish)
|
||||
|
||||
return thunk.waitForError()
|
||||
return (await publication.settled()).getError()
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
eventWriter.setIdentifier().setCollaborative(true)
|
||||
}
|
||||
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(eventWriter).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
back()
|
||||
} else {
|
||||
pushToast({message: "Shelf created!"})
|
||||
navigate(makeLibraryPath(url, getAddress(thunk.options.event)))
|
||||
navigate(makeLibraryPath(url, getAddress(publication.event)))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {POLL_RESPONSE, tagSpec, tagValues, relay} from "@welshman/util"
|
||||
import {Poll, PollResponse} from "@welshman/domain"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import type {Publication} from "@welshman/app"
|
||||
import PollOption from "@app/components/PollOption.svelte"
|
||||
import {command, events, reader, thunks, user, writer} from "@app/core"
|
||||
import {command, events, publisher, reader, user, writer} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
}
|
||||
|
||||
const publishSelection = async (selection: string[]) => {
|
||||
activeThunk?.abort()
|
||||
activeThunk = undefined
|
||||
activePublication?.abort()
|
||||
activePublication = undefined
|
||||
|
||||
if (selection.length > 0) {
|
||||
const eventWriter = writer(PollResponse).forceRoutes(relay(url)).setPollId(event.id)
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
const responseCommand = await command(eventWriter)
|
||||
|
||||
// Give the user time to check more boxes before the vote actually goes out.
|
||||
activeThunk = $thunks.publish({
|
||||
activePublication = $publisher.publish({
|
||||
event: responseCommand.event,
|
||||
relays: responseCommand.relays,
|
||||
delay: pollType === "multiplechoice" ? 1000 : undefined,
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
|
||||
const poll = reader(Poll)(event)
|
||||
let selectedIds = $state<string[]>([])
|
||||
let activeThunk: Thunk | undefined
|
||||
let activePublication: Publication | undefined
|
||||
|
||||
const pollType = poll.pollType()
|
||||
const results = $derived(poll.results($responses))
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
})
|
||||
|
||||
onDestroy(() => {
|
||||
activeThunk?.abort()
|
||||
activePublication?.abort()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {logout} from "@app/session"
|
||||
import {app, relayLists, thunks, user, writer} from "@app/core"
|
||||
import {app, publisher, relayLists, user, writer} from "@app/core"
|
||||
|
||||
let progress: number | undefined = $state(undefined)
|
||||
let confirmText = $state("")
|
||||
|
|
@ -51,12 +51,12 @@
|
|||
}
|
||||
|
||||
// First, blank out their profile in case relays don't support deletion by address
|
||||
await $thunks.publish({relays, event: profileEvent})
|
||||
await $publisher.publish({relays, event: profileEvent})
|
||||
|
||||
await incrementProgress()
|
||||
|
||||
// Next, send a "right to vanish" event to all relays
|
||||
await $thunks.publish({relays, event: vanishEvent})
|
||||
await $publisher.publish({relays, event: vanishEvent})
|
||||
|
||||
await incrementProgress()
|
||||
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
await $thunks.publish({relays, event: makeEvent(DELETE, {tags})})
|
||||
await $publisher.publish({relays, event: makeEvent(DELETE, {tags})})
|
||||
|
||||
await incrementProgress()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
try {
|
||||
const command = await $profiles.update(writer => writer.update(profile))
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
if (result?.url) {
|
||||
const command = await $profiles.update(w => w.setBanner(result.url))
|
||||
|
||||
await command.publish().waitForCompletion()
|
||||
await command.publish().settled()
|
||||
|
||||
pushToast({message: "Banner updated."})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,36 +2,40 @@
|
|||
import cx from "classnames"
|
||||
import {stopPropagation} from "svelte/legacy"
|
||||
import {noop} from "@welshman/lib"
|
||||
import type {BaseThunk} from "@welshman/app"
|
||||
import {PublicationGroup} from "@welshman/app"
|
||||
import type {Publishable} from "@welshman/app"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
|
||||
import {thunks} from "@app/core"
|
||||
import PublicationToast from "@app/components/PublicationToast.svelte"
|
||||
import PublicationStatusDetail from "@app/components/PublicationStatusDetail.svelte"
|
||||
import {publisher} from "@app/core"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
thunk: BaseThunk
|
||||
publication: Publishable
|
||||
showToastOnRetry?: boolean
|
||||
// See ThunkPending's `inline` — same trade-off, same default.
|
||||
// See PublicationPending's `inline` — same trade-off, same default.
|
||||
inline?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {thunk, showToastOnRetry, inline = false, ...restProps}: Props = $props()
|
||||
const {publication, showToastOnRetry, inline = false, ...restProps}: Props = $props()
|
||||
|
||||
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
|
||||
const showFailure = $derived($publication.isComplete() && $publication.getFailedUrls().length > 0)
|
||||
|
||||
const retry = (url: string) => {
|
||||
const retried = $thunks.retry(thunk, [url])
|
||||
const retried =
|
||||
publication instanceof PublicationGroup
|
||||
? $publisher.retry(publication, [url])
|
||||
: $publisher.retry(publication, [url])
|
||||
|
||||
if (showToastOnRetry) {
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk: retried},
|
||||
component: PublicationToast,
|
||||
props: {publication: retried},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -44,8 +48,8 @@
|
|||
onclick={stopPropagation(noop)}>
|
||||
<Tippy
|
||||
class="flex items-center"
|
||||
component={ThunkStatusDetail}
|
||||
props={{thunk, retry}}
|
||||
component={PublicationStatusDetail}
|
||||
props={{publication, retry}}
|
||||
params={{interactive: true, maxWidth: "none", trigger: "click"}}>
|
||||
<span class="flex cursor-pointer items-center gap-1 opacity-75">
|
||||
<Icon icon={Danger} class="text-error" size={3} />
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {stopPropagation} from "svelte/legacy"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import type {BaseThunk} from "@welshman/app"
|
||||
import type {Publishable} from "@welshman/app"
|
||||
|
||||
type Props = {
|
||||
thunk: BaseThunk
|
||||
publication: Publishable
|
||||
// A standalone usage fills its own row, and forcing that inside a flex row costs that row a line.
|
||||
inline?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {thunk, inline = false, ...restProps}: Props = $props()
|
||||
const {publication, inline = false, ...restProps}: Props = $props()
|
||||
|
||||
const abort = () => thunk.abort()
|
||||
const abort = () => publication.abort()
|
||||
|
||||
const isSending = $derived($thunk.hasStatus(PublishStatus.Sending))
|
||||
const canAbort = $derived($publication.canAbort())
|
||||
</script>
|
||||
|
||||
<div class={cx("flex px-1 text-xs", {"w-full justify-end": !inline}, restProps.class)}>
|
||||
|
|
@ -25,8 +24,8 @@
|
|||
<button
|
||||
type="button"
|
||||
class={cx("underline transition-all", {
|
||||
link: isSending,
|
||||
"pointer-events-none opacity-25": !isSending,
|
||||
link: canAbort,
|
||||
"pointer-events-none opacity-25": !canAbort,
|
||||
})}
|
||||
onclick={stopPropagation(abort)}>
|
||||
Cancel
|
||||
23
src/app/components/PublicationStatus.svelte
Normal file
23
src/app/components/PublicationStatus.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type {Publishable} from "@welshman/app"
|
||||
import PublicationFailure from "@app/components/PublicationFailure.svelte"
|
||||
import PublicationPending from "@app/components/PublicationPending.svelte"
|
||||
|
||||
type Props = {
|
||||
publication: Publishable
|
||||
showToastOnRetry?: boolean
|
||||
inline?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {publication, showToastOnRetry, inline = false, ...restProps}: Props = $props()
|
||||
|
||||
const showFailure = $derived($publication.isComplete() && $publication.getFailedUrls().length > 0)
|
||||
const showPending = $derived(!$publication.isComplete())
|
||||
</script>
|
||||
|
||||
{#if showFailure}
|
||||
<PublicationFailure class={restProps.class} {publication} {showToastOnRetry} {inline} />
|
||||
{:else if showPending}
|
||||
<PublicationPending class={restProps.class} {publication} {inline} />
|
||||
{/if}
|
||||
|
|
@ -2,21 +2,21 @@
|
|||
import {stopPropagation} from "svelte/legacy"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {BaseThunk} from "@welshman/app"
|
||||
import type {Publishable} from "@welshman/app"
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
||||
type Props = {
|
||||
thunk: BaseThunk
|
||||
publication: Publishable
|
||||
retry: (url: string) => void
|
||||
}
|
||||
|
||||
const {thunk, retry}: Props = $props()
|
||||
const {publication, retry}: Props = $props()
|
||||
|
||||
const successUrls = $derived($thunk.getUrlsWithStatus(PublishStatus.Success))
|
||||
const failedUrls = $derived($thunk.getFailedUrls())
|
||||
const successUrls = $derived($publication.getUrlsWithStatus(PublishStatus.Success))
|
||||
const failedUrls = $derived($publication.getFailedUrls())
|
||||
const total = $derived(successUrls.length + failedUrls.length)
|
||||
const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0)
|
||||
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
{/each}
|
||||
{#each failedUrls as url (url)}
|
||||
{@const {detail, status} = $thunk.results[url] || {}}
|
||||
{@const {detail, status} = $publication.results[url] || {}}
|
||||
<div class="grid grid-cols-[1rem_1fr_auto] items-start gap-x-3 gap-y-1 text-sm">
|
||||
<Icon icon={Danger} class="mt-0.5 text-error" size={4} />
|
||||
<div class="min-w-0">
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
import type {Snippet} from "svelte"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
import {thunks} from "@app/core"
|
||||
import PublicationStatus from "@app/components/PublicationStatus.svelte"
|
||||
import {publisher} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||
import {noPublications, publicationsByEventId} from "@app/publications"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
|
|
@ -16,15 +16,15 @@
|
|||
|
||||
const {event, context, status, children}: Props = $props()
|
||||
const deleted = $derived(context.deleted(event))
|
||||
const pending = $derived($thunksByEventId.get(event.id) ?? noThunks)
|
||||
const thunk = $derived($thunks.merge(pending))
|
||||
const pending = $derived($publicationsByEventId.get(event.id) ?? noPublications)
|
||||
const group = $derived($publisher.merge(pending))
|
||||
</script>
|
||||
|
||||
{#if $deleted}
|
||||
<div class="button button-error button-xs rounded-full">Deleted</div>
|
||||
{:else}
|
||||
{#if $thunk.thunks.length > 0 && !$thunk.hasStatus(PublishStatus.Success)}
|
||||
<ThunkStatus {thunk} inline />
|
||||
{#if $group.publications.length > 0 && !$group.hasStatus(PublishStatus.Success)}
|
||||
<PublicationStatus publication={group} inline />
|
||||
{:else}
|
||||
{@render status?.()}
|
||||
{/if}
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type {BaseThunk} from "@welshman/app"
|
||||
import type {Publishable} from "@welshman/app"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import ThunkPending from "@app/components/ThunkPending.svelte"
|
||||
import PublicationPending from "@app/components/PublicationPending.svelte"
|
||||
import type {Toast} from "@app/toast"
|
||||
import {popToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
toast: Toast
|
||||
thunk: BaseThunk
|
||||
publication: Publishable
|
||||
}
|
||||
|
||||
const {toast, ...props}: Props = $props()
|
||||
|
||||
const id = toast.id
|
||||
const thunk = props.thunk
|
||||
const publication = props.publication
|
||||
const {Aborted, Timeout, Failure} = PublishStatus
|
||||
const isFailure = $derived($thunk.hasStatus([Aborted, Timeout, Failure]))
|
||||
const isComplete = $derived($thunk.isComplete())
|
||||
const isFailure = $derived($publication.hasStatus([Aborted, Timeout, Failure]))
|
||||
const isComplete = $derived($publication.isComplete())
|
||||
|
||||
$effect(() => {
|
||||
if (isFailure) {
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</script>
|
||||
|
||||
{#if !isComplete}
|
||||
<ThunkPending {thunk} />
|
||||
<PublicationPending {publication} />
|
||||
{:else if !isFailure}
|
||||
<p class="text-xs opacity-75">Message sent!</p>
|
||||
{/if}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
import type {Readable} from "svelte/store"
|
||||
import {tryCatch, uniq} from "@welshman/lib"
|
||||
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import type {Publication} from "@welshman/app"
|
||||
import {Relays} from "@welshman/app"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {errorMessage} from "@lib/util"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
type Props = {
|
||||
relays: Readable<string[]>
|
||||
addRelay: (url: string) => Promise<Thunk>
|
||||
addRelay: (url: string) => Promise<Publication>
|
||||
matchRelay?: (url: string) => boolean
|
||||
}
|
||||
|
||||
|
|
@ -36,8 +36,8 @@
|
|||
loading.add(url)
|
||||
|
||||
try {
|
||||
const thunk = await addRelay(url)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await addRelay(url)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type {Readable} from "svelte/store"
|
||||
import {SvelteSet} from "svelte/reactivity"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import type {Publication} from "@welshman/app"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
title: string
|
||||
subtitle: string
|
||||
relays: Readable<string[]>
|
||||
addRelay: (url: string) => Promise<Thunk>
|
||||
removeRelay: (url: string) => Promise<Thunk>
|
||||
addRelay: (url: string) => Promise<Publication>
|
||||
removeRelay: (url: string) => Promise<Publication>
|
||||
matchRelay?: (url: string) => boolean
|
||||
}
|
||||
|
||||
|
|
@ -38,8 +38,8 @@
|
|||
loading.add(url)
|
||||
|
||||
try {
|
||||
const thunk = await removeRelay(url)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await removeRelay(url)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
.setContent(content)
|
||||
|
||||
const reportCommand = await command(eventWriter)
|
||||
const error = await reportCommand.publishToRelays([url]).waitForError()
|
||||
const error = (await reportCommand.publishToRelays([url]).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
import RoomMembershipButton from "@app/components/RoomMembershipButton.svelte"
|
||||
import RoomPinnedMessages from "@app/components/RoomPinnedMessages.svelte"
|
||||
import RoomTranscript from "@app/components/RoomTranscript.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import PublicationToast from "@app/components/PublicationToast.svelte"
|
||||
import CallControlBar from "@app/components/CallControlBar.svelte"
|
||||
import {deletes, relays, rooms, thunks, user, writer} from "@app/core"
|
||||
import {deletes, publisher, relays, rooms, user, writer} from "@app/core"
|
||||
import {
|
||||
CallState,
|
||||
callState,
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
|
||||
const delay = await loadSendDelay()
|
||||
|
||||
const thunk = $thunks.publish({
|
||||
const publication = $publisher.publish({
|
||||
relays: [url],
|
||||
event: stamp(await eventWriter.renderTemplate(), edit?.created_at),
|
||||
delay,
|
||||
|
|
@ -141,8 +141,8 @@
|
|||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk},
|
||||
component: PublicationToast,
|
||||
props: {publication},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@
|
|||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||
confirm: async () => {
|
||||
const command = await $rooms.deleteRoom(url, {h})
|
||||
const thunk = command.publish()
|
||||
const message = await thunk.waitForError()
|
||||
const publication = command.publish()
|
||||
const message = (await publication.settled()).getError()
|
||||
|
||||
if (message) {
|
||||
$app.repository.removeEvent(thunk.options.event.id)
|
||||
$app.repository.removeEvent(publication.event.id)
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
await $roomLists.removeRoom(h, url).then(publish)
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@
|
|||
room.picture = await resolveImageInput(imageFile, imagePreview, compressOptions)
|
||||
|
||||
const createCommand = await $rooms.createRoom(url, room)
|
||||
const createMessage = await createCommand.publish().waitForError()
|
||||
const createMessage = (await createCommand.publish().settled()).getError()
|
||||
|
||||
if (createMessage && !createMessage.includes("already")) {
|
||||
return pushToast({theme: "error", message: createMessage})
|
||||
}
|
||||
|
||||
const editCommand = await $rooms.editRoom(url, room)
|
||||
const editMessage = await editCommand.publish().waitForError()
|
||||
const editMessage = (await editCommand.publish().settled()).getError()
|
||||
|
||||
if (editMessage) {
|
||||
return pushToast({theme: "error", message: editMessage})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import PublicationFailure from "@app/components/PublicationFailure.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import {deriveDisplaysByPubkey} from "@app/social"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
import RoomItemMenu from "@app/components/RoomItemMenu.svelte"
|
||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||
import {events, profiles, reader, thunks, user} from "@app/core"
|
||||
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||
import {events, profiles, publisher, reader, user} from "@app/core"
|
||||
import {noPublications, publicationsByEventId} from "@app/publications"
|
||||
import {colorFor} from "@app/theme"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
|
|
@ -56,7 +56,9 @@
|
|||
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profileDisplay = $profiles.display(event.pubkey, [url]).$
|
||||
const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks))
|
||||
const publication = $derived(
|
||||
$publisher.merge($publicationsByEventId.get(event.id) ?? noPublications),
|
||||
)
|
||||
const colorValue = colorFor(event.pubkey)
|
||||
|
||||
// A message that is nothing but a reference renders as the event it quotes.
|
||||
|
|
@ -123,8 +125,8 @@
|
|||
{/if}
|
||||
<div class={cx({"mt-2": showPubkey && event.kind !== MESSAGE})}>
|
||||
<RoomItemContent {url} event={$innerEvent ?? event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-1 flex justify-end" />
|
||||
{#if publication}
|
||||
<PublicationFailure showToastOnRetry {publication} class="mt-1 flex justify-end" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
message: "Are you sure you want to remove this user from the room?",
|
||||
confirm: async () => {
|
||||
const command = await $rooms.removeMember(url, {h}, pubkey)
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import type {ClientOptions} from "@pomade/core"
|
||||
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||
import {Profile} from "@welshman/domain"
|
||||
import {Thunks, nip01, publish, toSession} from "@welshman/app"
|
||||
import {Publisher, nip01, publish, toSession} from "@welshman/app"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import {getKey, setKey} from "@lib/implicit"
|
||||
|
|
@ -45,16 +45,16 @@
|
|||
await Promise.all(DEFAULT_SPACES.map(url => attemptRelayAccess(url)))
|
||||
|
||||
// Add default outbox/inbox/messaging relays, profile, spaces
|
||||
const thunks = await Promise.all([
|
||||
$app.use(Thunks).publish({
|
||||
const publications = await Promise.all([
|
||||
$app.use(Publisher).publish({
|
||||
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
|
||||
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS, ...DEFAULT_SPACES],
|
||||
}),
|
||||
$app.use(Thunks).publish({
|
||||
$app.use(Publisher).publish({
|
||||
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
|
||||
relays: [...DEFAULT_RELAYS, ...DEFAULT_SPACES],
|
||||
}),
|
||||
$app.use(Thunks).publish({
|
||||
$app.use(Publisher).publish({
|
||||
event: await $domain
|
||||
.writer(Profile)
|
||||
.update(getKey<ProfileValues>("signup.profile")!)
|
||||
|
|
@ -64,8 +64,8 @@
|
|||
$roomLists.setRelays(DEFAULT_SPACES).then(publish),
|
||||
])
|
||||
|
||||
// Wait for all the thunks to complete
|
||||
await Promise.all(thunks.map(thunk => thunk.waitForCompletion()))
|
||||
// Wait for all the publications to settle
|
||||
await Promise.all(publications.map(publication => publication.settled()))
|
||||
|
||||
// Go to the dashboard
|
||||
clearModals()
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
<script lang="ts">
|
||||
import {BaseThunk} from "@welshman/app"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ThunkPending from "@app/components/ThunkPending.svelte"
|
||||
|
||||
type Props = {
|
||||
thunk: BaseThunk
|
||||
showToastOnRetry?: boolean
|
||||
inline?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {thunk, showToastOnRetry, inline = false, ...restProps}: Props = $props()
|
||||
|
||||
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
|
||||
const showPending = $derived(!$thunk.isComplete())
|
||||
</script>
|
||||
|
||||
{#if showFailure}
|
||||
<ThunkFailure class={restProps.class} {thunk} {showToastOnRetry} {inline} />
|
||||
{:else if showPending}
|
||||
<ThunkPending class={restProps.class} {thunk} {inline} />
|
||||
{/if}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
try {
|
||||
const command = await $profiles.update(writer => writer.update({lud06: undefined, lud16}))
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`})
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
writer.update({lud06: undefined, lud16: address.trim() || undefined}),
|
||||
)
|
||||
|
||||
const error = await command.publish().waitForError()
|
||||
const error = (await command.publish().settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
Network,
|
||||
PinLists,
|
||||
Profiles,
|
||||
Publisher,
|
||||
Reactions,
|
||||
RelayLists,
|
||||
RelayManagement,
|
||||
|
|
@ -31,7 +32,6 @@ import {
|
|||
Rooms,
|
||||
Router,
|
||||
SearchRelayLists,
|
||||
Thunks,
|
||||
User,
|
||||
Wot,
|
||||
Wraps,
|
||||
|
|
@ -140,6 +140,7 @@ export const muteLists = usePlugin(MuteLists)
|
|||
export const network = usePlugin(Network)
|
||||
export const pinLists = usePlugin(PinLists)
|
||||
export const profiles = usePlugin(Profiles)
|
||||
export const publisher = usePlugin(Publisher)
|
||||
export const reactions = usePlugin(Reactions)
|
||||
export const relayLists = usePlugin(RelayLists)
|
||||
export const relayManagement = usePlugin(RelayManagement)
|
||||
|
|
@ -152,7 +153,6 @@ export const roomPinLists = usePlugin(RoomPinLists)
|
|||
export const rooms = usePlugin(Rooms)
|
||||
export const router = usePlugin(Router)
|
||||
export const searchRelayLists = usePlugin(SearchRelayLists)
|
||||
export const thunks = usePlugin(Thunks)
|
||||
export const wot = usePlugin(Wot)
|
||||
export const wraps = usePlugin(Wraps)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ import {
|
|||
matchReason,
|
||||
RelayReasonPrefix,
|
||||
} from "@welshman/net"
|
||||
import {BlockedRelayLists, MessagingRelayLists, RelayLists, RoomLists, Thunks} from "@welshman/app"
|
||||
import {
|
||||
BlockedRelayLists,
|
||||
MessagingRelayLists,
|
||||
RelayLists,
|
||||
Publisher,
|
||||
RoomLists,
|
||||
} from "@welshman/app"
|
||||
import type {AppPolicy, IApp} from "@welshman/app"
|
||||
import {merged} from "@welshman/store"
|
||||
import {logger, appPolicies} from "@app/core"
|
||||
|
|
@ -75,7 +81,7 @@ const shouldAuth = (socket: Socket, $app: IApp) => {
|
|||
if ($app.use(RelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||
return true
|
||||
}
|
||||
if (get($app.use(Thunks).history).some(t => t.options.relays.includes(socket.url))) {
|
||||
if (get($app.use(Publisher).history).some(p => p.relays.includes(socket.url))) {
|
||||
return true
|
||||
}
|
||||
if ($app.use(MessagingRelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||
|
|
@ -92,7 +98,7 @@ const makeAuthInputs = ($app: IApp, pubkey: string) =>
|
|||
$app.use(MessagingRelayLists).urls(pubkey).$,
|
||||
$app.use(RelayLists).urls(pubkey).$,
|
||||
$app.use(RoomLists).urls(pubkey).$,
|
||||
$app.use(Thunks).history,
|
||||
$app.use(Publisher).history,
|
||||
userSettingsValues,
|
||||
])
|
||||
|
||||
|
|
|
|||
36
src/app/publications.ts
Normal file
36
src/app/publications.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {derived} from "svelte/store"
|
||||
import {pushToMapKey} from "@welshman/lib"
|
||||
import {Publisher} from "@welshman/app"
|
||||
import type {Publication} from "@welshman/app"
|
||||
import {fromApp} from "@app/core"
|
||||
|
||||
let previous = new Map<string, Publication[]>()
|
||||
|
||||
// Indexed once, since each row filtering the whole history is O(rows x history) per publish.
|
||||
export const publicationsByEventId = derived(
|
||||
fromApp($app => $app.use(Publisher).history),
|
||||
$history => {
|
||||
const byId = new Map<string, Publication[]>()
|
||||
|
||||
for (const publication of $history) {
|
||||
pushToMapKey(byId, publication.event.id, publication)
|
||||
}
|
||||
|
||||
// Hand back last time's array where an event's publications are unchanged, so the merge below is stable.
|
||||
for (const [id, publications] of byId) {
|
||||
const before = previous.get(id)
|
||||
|
||||
if (before?.length === publications.length && before.every((p, i) => p === publications[i])) {
|
||||
byId.set(id, before)
|
||||
}
|
||||
}
|
||||
|
||||
previous = byId
|
||||
|
||||
return byId
|
||||
},
|
||||
new Map<string, Publication[]>(),
|
||||
)
|
||||
|
||||
// Shared, so a row with nothing in flight keeps the same value across every publish.
|
||||
export const noPublications: Publication[] = []
|
||||
|
|
@ -4,7 +4,7 @@ import {assoc, hash, maybe, ms, poll} from "@welshman/lib"
|
|||
import type {Filter} from "@welshman/util"
|
||||
import {Address, DELETE, makeEvent} from "@welshman/util"
|
||||
import {Relays, User} from "@welshman/app"
|
||||
import {app, messagingRelayLists, roomLists, thunks} from "@app/core"
|
||||
import {app, messagingRelayLists, publisher, roomLists} from "@app/core"
|
||||
import {device} from "@app/device"
|
||||
import {PUSH_BRIDGE} from "@app/env"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
|
|
@ -123,7 +123,7 @@ export class CapacitorNotifications implements IPushAdapter {
|
|||
|
||||
const identifier = this._getSubscriptionIdentifier(relay, key)
|
||||
|
||||
const thunk = thunks.get().publish({
|
||||
const publication = publisher.get().publish({
|
||||
relays: [url],
|
||||
event: makeEvent(PUSH_SUBSCRIPTION, {
|
||||
tags: [
|
||||
|
|
@ -137,7 +137,7 @@ export class CapacitorNotifications implements IPushAdapter {
|
|||
}),
|
||||
})
|
||||
|
||||
const error = await thunk.waitForError()
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
console.warn(`Failed to subscribe ${relay} to ${key} notifications:`, error)
|
||||
|
|
@ -160,7 +160,8 @@ export class CapacitorNotifications implements IPushAdapter {
|
|||
identifier,
|
||||
).toString()
|
||||
const event = makeEvent(DELETE, {tags: [["a", address]]})
|
||||
const error = await thunks.get().publish({relays, event}).waitForError()
|
||||
const publication = publisher.get().publish({relays, event})
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
console.warn(`Failed to unsubscribe ${relay} from notifications:`, error)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const toggleRoomPin = async (url: string, h: string, id: string) => {
|
|||
const command = await roomPinLists
|
||||
.get()
|
||||
.setPins(url, h, wasPinned ? pins.filter(pin => pin !== id) : [...pins, id])
|
||||
const error = await command.publishToRelays([url]).waitForError()
|
||||
const error = (await command.publishToRelays([url]).settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
roomLists,
|
||||
rooms,
|
||||
router,
|
||||
thunks,
|
||||
publisher,
|
||||
relays,
|
||||
user,
|
||||
writer,
|
||||
|
|
@ -59,7 +59,8 @@ export const addRoomMembers = async (url: string, room: RoomMeta, pubkeys: strin
|
|||
rooms
|
||||
.get()
|
||||
.addMember(url, room, pk)
|
||||
.then(command => command.publish().waitForError()),
|
||||
.then(command => command.publish().settled())
|
||||
.then(publication => publication.getError()),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ export const publishRoomQuote = async ({
|
|||
}) => {
|
||||
const eventWriter = await addressWriterToSpace(writer(Message).setParent(parent), url, h)
|
||||
|
||||
return thunks.get().publish({
|
||||
return publisher.get().publish({
|
||||
relays: [url],
|
||||
event: await eventWriter.renderTemplate(),
|
||||
delay,
|
||||
|
|
@ -145,7 +146,7 @@ export const publishComment = async ({
|
|||
tagValue(tagSpec("h"), event.tags),
|
||||
)
|
||||
|
||||
return thunks.get().publish({
|
||||
return publisher.get().publish({
|
||||
relays: [url],
|
||||
event: await eventWriter.renderTemplate(),
|
||||
delay: await loadSendDelay(),
|
||||
|
|
@ -156,14 +157,14 @@ export const publishToSpace = async (
|
|||
eventWriter: EventWriter<BaseEventReader>,
|
||||
{url, h, shareToChat}: {url: string; h?: string; shareToChat?: boolean},
|
||||
) => {
|
||||
const thunk = await command(await addressWriterToSpace(eventWriter, url, h)).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await command(await addressWriterToSpace(eventWriter, url, h)).then(publish)
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (shareToChat && !error) {
|
||||
publishRoomQuote({url, h, parent: thunk.options.event})
|
||||
publishRoomQuote({url, h, parent: publication.event})
|
||||
}
|
||||
|
||||
return {thunk, error}
|
||||
return {publication, error}
|
||||
}
|
||||
|
||||
// User
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
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[]>()
|
||||
|
||||
// Indexed once, since each row filtering the whole history is O(rows x history) per publish.
|
||||
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.options.event.id, thunk)
|
||||
}
|
||||
|
||||
// Hand back last time's array where an event's thunks are unchanged, so the merge below is stable.
|
||||
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.
|
||||
export const noThunks: Thunk[] = []
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
sendingLogs = true
|
||||
|
||||
try {
|
||||
const thunk = await sendLogs()
|
||||
const error = await thunk.waitForError()
|
||||
const publication = await sendLogs()
|
||||
const error = (await publication.settled()).getError()
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
|
|
|
|||
Loading…
Reference in a new issue