This commit is contained in:
+71
-60
@@ -2,82 +2,93 @@
|
||||
|
||||
[](https://npmjs.com/package/@welshman/app)
|
||||
|
||||
A comprehensive framework for building nostr clients, powering production applications like [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social). It provides a complete toolkit for managing events, subscriptions, user data, and relay connections.
|
||||
An instance-based, composable client for building nostr applications. It powers production clients like [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social), and ties together the rest of the welshman packages (`util`, `net`, `store`, `router`, `signer`, `feeds`) into a single, cohesive app layer.
|
||||
|
||||
## What's Included
|
||||
## The core idea: an app is an `App` instance
|
||||
|
||||
- **Repository** - Event storage and query capabilities
|
||||
- **Router** - Intelligent relay selection for optimal network access
|
||||
- **Feed Controller** - Manages feed loading
|
||||
- **Session Management** - User identity and key management
|
||||
- **Event Actions** - High-level operations like reacting, replying, etc.
|
||||
- **Profile Management** - User profile handling and metadata
|
||||
- **Relay Directory** - Discovery and management of relays
|
||||
- **Web of Trust** - Utilities for building webs of trust
|
||||
|
||||
## Quick Example
|
||||
Everything in `@welshman/app` hangs off a single `App` instance. An `App` owns the per-identity primitives — an event `Repository`, a connection `Pool`, a `Tracker`, and a `WrapManager` — plus a `config` and (optionally) a signed-in `User`. Because all state lives on the instance, two apps never share data: you can run multiple identities side-by-side, and tearing one down with `cleanup()` releases everything it allocated.
|
||||
|
||||
```typescript
|
||||
import {getNip07} from '@welshman/signer'
|
||||
import {load, request, RequestEvent, defaultSocketPolicies, makeSocketPolicyAuth, Socket} from '@welshman/net'
|
||||
import {StampedEvent, TrustedEvent, makeEvent, NOTE} from '@welshman/util'
|
||||
import {pubkey, signer, publishThunk} from '@welshman/app'
|
||||
import {createApp} from "@welshman/app"
|
||||
|
||||
// Log in via NIP 07
|
||||
addSession({method: 'nip07', pubkey: await getNip07().getPubkey()})
|
||||
// A batteries-included app (event ingestion, relay stats, gift-wrap
|
||||
// unwrapping, and NIP-42 auth are all wired up by default policies)
|
||||
const app = createApp()
|
||||
```
|
||||
|
||||
// Enable automatic authentication to relays
|
||||
defaultSocketPolicies.push(
|
||||
makeSocketPolicyAuth({
|
||||
sign: (event: StampedEvent) => signer.get()?.sign(event),
|
||||
shouldAuth: (socket: Socket) => true,
|
||||
}),
|
||||
)
|
||||
Features are exposed as **plugins** — lazily-constructed singletons resolved through `app.use(...)`:
|
||||
|
||||
// This will fetch the user's profile automatically, and return a store that updates
|
||||
// automatically. Several different stores exist that are ready to go, including handles,
|
||||
// zappers, relayLists, relays, follows, mutes.
|
||||
const profile = deriveProfile(pubkey.get())
|
||||
```typescript
|
||||
import {createApp, Profiles, RelayLists, Thunks} from "@welshman/app"
|
||||
|
||||
// Publish is done using thunks, which optimistically publish to the local database, deferring
|
||||
// signing and publishing for instant user feedback. Progress is reported as relays accept/reject the event
|
||||
// Events are automatically signed using the current session
|
||||
const thunk = publishThunk({
|
||||
relays: Router.get().FromUser().getUrls(),
|
||||
const app = createApp()
|
||||
|
||||
// Each plugin is constructed once per app and memoized
|
||||
const profiles = app.use(Profiles)
|
||||
const relayLists = app.use(RelayLists)
|
||||
```
|
||||
|
||||
This replaces the previous global-singleton design (`pubkey`, `deriveProfile`, `publishThunk`, `Router.get()`). There are no module-level globals anymore — you create an app and reach everything through it.
|
||||
|
||||
## Architecture at a glance
|
||||
|
||||
| Layer | What it is | Where |
|
||||
|---|---|---|
|
||||
| **`App`** | The app instance; owns repository/pool/tracker/wrapManager and the `use()` registry | [App](./app) |
|
||||
| **`User` & sessions** | The signed-in identity and serializable login descriptors | [User & Sessions](./user) |
|
||||
| **Policies** | Side effects installed at construction (ingest, auth, stats, wraps) | [App](./apppolicies) |
|
||||
| **Plugins** | Lazily-resolved feature modules built on a small set of base classes | [Plugin architecture](./plugins) |
|
||||
| **Data plugins** | Reactive collections of profiles, lists, relays, handles, zappers… | [Data](./data) |
|
||||
| **Publishing** | Optimistic publishing via thunks | [Publishing](./publishing) |
|
||||
| **Requests** | Loading & negentropy sync | [Requests](./requests) |
|
||||
| **Routing** | Outbox-model relay selection and tag builders | [Routing](./routing) |
|
||||
| **Web of Trust** | Follow/mute graph scoring | [Web of Trust](./wot) |
|
||||
| **Feeds & Search** | Feed controllers and fuzzy search | [Feeds & Search](./feeds-and-search) |
|
||||
|
||||
## Quick example
|
||||
|
||||
```typescript
|
||||
import {createApp, User, toSession, nip07, Profiles, Thunks, Router} from "@welshman/app"
|
||||
import {getNip07} from "@welshman/signer"
|
||||
import {makeEvent, NOTE} from "@welshman/util"
|
||||
import {addMinimalFallbacks} from "@welshman/router"
|
||||
|
||||
// 1. Log in. A session is a serializable {method, data} descriptor; User
|
||||
// turns it back into a live, signing identity.
|
||||
const pubkey = await getNip07().getPubkey()
|
||||
const session = toSession(nip07, {})
|
||||
const user = await User.fromSession(session)
|
||||
|
||||
// 2. Create the app around that user.
|
||||
const app = createApp({user})
|
||||
|
||||
// 3. Read data reactively. Stores lazily fetch over the network using the
|
||||
// outbox model and update as events arrive.
|
||||
const profile = app.use(Profiles).one(pubkey) // Readable<Maybe<Profile>>
|
||||
profile.subscribe($profile => console.log($profile?.name))
|
||||
|
||||
// 4. Publish optimistically. The event is written to the local repository
|
||||
// immediately, signed lazily, and progress is reported per-relay.
|
||||
const thunk = app.use(Thunks).publishToOutbox({
|
||||
event: makeEvent(NOTE, {content: "hi"}),
|
||||
delay: 3000,
|
||||
delay: 3000, // soft-undo window
|
||||
})
|
||||
|
||||
// Thunks can be aborted until after `delay`, allowing for soft-undo
|
||||
thunk.controller.abort()
|
||||
// Abort before `delay` elapses to undo
|
||||
// thunk.abort()
|
||||
await thunk.waitForCompletion()
|
||||
|
||||
// Some commands are included
|
||||
const thunk = follow(['p', '97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322'])
|
||||
|
||||
// Load events as a promise
|
||||
const events = await load({
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
}])
|
||||
|
||||
// Or use `request` for more fine-grained subscription control
|
||||
const abortController = new AbortController()
|
||||
|
||||
request({
|
||||
signal: abortController.signal,
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
console.log(event)
|
||||
},
|
||||
}])
|
||||
|
||||
// Close the request
|
||||
abortController.abort()
|
||||
// 5. Tear it all down
|
||||
app.cleanup()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @welshman/app
|
||||
# or
|
||||
pnpm add @welshman/app
|
||||
yarn add @welshman/app
|
||||
```
|
||||
|
||||
`@welshman/app` has peer dependencies on `svelte` (4 or 5) and the other welshman workspace packages (`@welshman/feeds`, `@welshman/lib`, `@welshman/net`, `@welshman/router`, `@welshman/signer`, `@welshman/store`, `@welshman/util`), plus `@pomade/core` for the optional Pomade signer.
|
||||
|
||||
Reference in New Issue
Block a user