Add vitepress docs

This commit is contained in:
Ticruz
2025-02-04 14:43:40 +01:00
committed by Jon Staab
parent 43255bcb74
commit 94375a56ec
84 changed files with 10821 additions and 139 deletions
+103
View File
@@ -0,0 +1,103 @@
# Collection Stores
The `collection` utility creates stores that handle caching, loading, and indexing of Nostr data. It provides a consistent pattern for managing entities that need to be fetched from the network and cached locally.
```typescript
const {
indexStore, // Map of all items by key
deriveItem, // Get reactive item by key
loadItem // Trigger network load
} = collection({
name: "storeName", // For persistence
store: writable([]), // Base store
getKey: item => item.id // How to index items
load: async (key) => { // Network loader
// Load logic here
}
})
```
## Available Collections
```typescript
// Profiles
profiles profilesByPubkey deriveProfile loadProfile
// Lists
follows followsByPubkey deriveFollows loadFollows
mutes mutesByPubkey deriveMutes loadMutes
pins pinsByPubkey derivePins loadPins
// Relays
relays relaysByUrl deriveRelay loadRelay
relaySelections relaySelectionsByPubkey deriveRelaySelections loadRelaySelections
inboxRelaySelections inboxRelaySelectionsByPubkey deriveInboxRelaySelections loadInboxRelaySelections
// Identity
handles handlesByNip05 deriveHandle loadHandle
zappers zappersByLnurl deriveZapper loadZapper
```
## Real World Examples
### Loading and Displaying Profiles
```typescript
import {
deriveProfile,
loadProfile,
displayProfile
} from '@welshman/app'
// In a Svelte component
let profile
// Subscribe to profile changes
$: profile = $deriveProfile(pubkey)
// Load automatically triggers when needed
onMount(() => {
loadProfile(pubkey, {
// Optional request params
relays: ["wss://relay.example.com"]
})
})
// Display with fallback
$: name = displayProfile(profile, "unknown")
```
### Managing Relay Selections
```typescript
import {
deriveRelaySelections,
loadRelaySelections,
getReadRelayUrls,
getWriteRelayUrls
} from '@welshman/app'
// Get user's relay preferences
const selections = deriveRelaySelections(pubkey).get()
// Load from network if needed
await loadRelaySelections(pubkey)
// Get read/write URLs
const readRelays = getReadRelayUrls(selections)
const writeRelays = getWriteRelayUrls(selections)
// Use with router
const relays = ctx.app.router
.FromPubkey(pubkey)
.getUrls()
```
Each collection automatically:
- Caches to IndexedDB
- Deduplicates network requests
- Updates reactively
- Provides typed access
- Handles loading states
The pattern is consistent across all stores, making it predictable to work with different types of nostr data.
+88
View File
@@ -0,0 +1,88 @@
# Commands
High-level commands for common Nostr operations.
Each command handles signing, encryption, and relay selection automatically.
## Available Commands
```typescript
// List Management
follow(pubkey)
unfollow(pubkey)
mute(pubkey)
unmute(pubkey)
pin(tag)
unpin(tag)
```
Each command returns a [`Thunk`](app/thunk) which:
- Optimistically updates local state
- Signs and publishes the event
- Can be aborted within a delay window
- Reports publish progress
## Real World Examples
### Following/Unfollowing Users
```typescript
import {follow, unfollow, userFollows} from '@welshman/app'
// Follow with optimistic update
const followUser = async (pubkey: string) => {
// Creates and publishes event with an updated follow list
const thunk = await follow(pubkey)
// Track publish status per relay
thunk.status.subscribe(statuses => {
for (const [url, status] of Object.entries(statuses)) {
console.log(`${url}: ${status}`)
}
})
// Can abort within delay window
setTimeout(() => thunk.controller.abort(), 1000)
}
// Unfollow works the same way
const unfollowUser = async (pubkey: string) => {
const thunk = await unfollow(pubkey)
// Wait for completion
const results = await thunk.result
}
```
### Managing Pins
```typescript
import {pin, unpin, userPins} from '@welshman/app'
// Pin an event with context
const pinEvent = async (event: TrustedEvent) => {
const thunk = await pin([
'e', event.id,
ctx.app.router.Event(event).getUrl()
])
// Handle specific relay errors
thunk.status.subscribe(statuses => {
for (const [url, {status, message}] of Object.entries(statuses)) {
if (status === 'failure') {
console.error(`Failed on ${url}: ${message}`)
}
}
})
}
```
All commands:
- Handle encryption automatically
- Select appropriate relays
- Update local state immediately
- Allow soft-undo via abort
- Report per-relay status
- Return consistent Thunk interface
Commands provide a high-level way to modify the Nostr state without dealing with the complexities of event creation, encryption, and relay selection.
+139
View File
@@ -0,0 +1,139 @@
# Application Context
The `@welshman/app` package uses a global context system to configure core behaviors.
Understanding the app context is essential as it powers [session/authentication](/app/session), [relay routing](/app/relay) and [request handling](/app/request).
## Basic Setup
```typescript
import {ctx, setContext} from '@welshman/lib'
import {getDefaultNetContext, getDefaultAppContext} from '@welshman/app'
// Initialize app with default settings
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext()
})
// Access context anywhere
console.log(ctx.app.router)
console.log(ctx.net.pool)
```
## Default App Context
```typescript
export type AppContext = {
// Smart relay routing system
router: Router
// Time to wait between batched requests (ms)
requestDelay: number // default: 50
// Time to wait for NIP-42 relay auth (ms)
authTimeout: number // default: 300
// Time to wait for request completion (ms)
requestTimeout: number // default: 3000
// URL of metadata service (optional)
dufflepudUrl?: string
// Additional relays for indexed content
indexerRelays?: string[]
}
// Example with custom settings
setContext({
app: getDefaultAppContext({
requestDelay: 100,
authTimeout: 500,
requestTimeout: 5000,
dufflepudUrl: "https://api.example.com",
indexerRelays: [
"wss://relay.example.com",
"wss://indexed.example.com"
]
})
})
```
## Network Context
```typescript
export type NetContext = {
// Global connection pool
pool: Pool
// How to handle NIP-42 auth
authMode: AuthMode // default: 'implicit'
// Event validation and handling
onEvent: (url: string, event: TrustedEvent) => void
isDeleted: (url: string, event: TrustedEvent) => boolean
isValid: (url: string, event: TrustedEvent) => boolean
// Event signing (used by all packages)
signEvent: (event: StampedEvent) => Promise<SignedEvent>
// Subscription optimization
optimizeSubscriptions: (subs: Subscription[]) => RelaysAndFilters[]
}
// Example with custom validation
setContext({
net: getDefaultNetContext({
// Custom event validation
isValid: (url, event) => {
if (url === LOCAL_RELAY_URL) return true
return hasValidSignature(event)
},
// Track deleted events
isDeleted: (url, event) =>
repository.isDeleted(event),
// Custom event handling
onEvent: (url, event) => {
// Save to local repository
repository.publish(event)
// Track which relay it came from
tracker.track(event.id, url)
}
})
})
```
## Using Context Values
Once configured, context values are used throughout the app:
```typescript
import {ctx} from '@welshman/lib'
// Smart relay routing
const relays = ctx.app.router
.ForPubkey(pubkey)
.getUrls()
// Publish with timeout
const pub = publish({
event,
relays,
timeout: ctx.app.requestTimeout
})
// Subscribe with auth
const sub = subscribe({
filters,
relays,
authTimeout: ctx.app.authTimeout
})
// Check connection pool
const connected = ctx.net.pool
.get(relay)
.socket.status === 'open'
```
+96
View File
@@ -0,0 +1,96 @@
# Feed
The feed system provides a powerful way to compose and load complex `Nostr` queries. It supports user scopes, web of trust filtering, DVM integration, and thread construction.
## Controller
The `controller.load()` function is the main interface for fetching events from a feed. It handles all the complexity of relay selection, subscription management, and event filtering.
```typescript
import {createFeedController} from '@welshman/app'
import {scopeFeed, wotFeed} from '@welshman/feeds'
const controller = createFeedController({
// Define what to load
feed: scopeFeed("follows"),
// Optional configurations
closeOnEose: true, // Close after getting all events
onEvent: event => {}, // Handle events as they arrive
onEose: url => {}, // Handle EOSE from each relay
onComplete: () => {}, // Called when all relays complete
})
// Load first 20 events
const events = await controller.load(20)
// Load next 20 events
const moreEvents = await controller.load(20)
```
The controller maintains its state between loads, so subsequent calls will:
- Continue from last position
- Use appropriate time windows
- Skip already seen events
- Maintain relay connections
## Paginated Feed
```typescript
import {intersectionFeed, scopeFeed, wotFeed} from '@welshman/feeds'
const HomeFeed = {
let events = []
let loading = false
let controller
onMount(() => {
// Create feed for home timeline
controller = createFeedController({
feed: intersectionFeed(
// Content from follows
scopeFeed("follows"),
// Filtered by web of trust
wotFeed({min: 0.1})
),
// Handle events as they arrive
onEvent: event => {
events = [...events, event]
},
// Track loading state
onComplete: () => {
loading = false
}
})
// Initial load
loadMore()
})
const loadMore = async () => {
if (loading) return
loading = true
// Load next batch
await controller.load(20)
}
}
```
Key points about `controller.load()`:
- Takes a limit parameter for batch size
- Returns a promise of loaded events
- Can be called repeatedly for pagination
- Handles subscription lifecycle
- Manages relay connections
- Deduplicates events
The controller is stateful and maintains:
- Current time window
- Seen events
- Active subscriptions
- Relay connections
This makes it ideal for implementing infinite scroll feeds, thread loading, and other paginated content scenarios.
+20
View File
@@ -0,0 +1,20 @@
# @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.
## What's Included
- **Repository System** - Event storage and query capabilities
- **Router** - Intelligent relay selection for optimal networking
- **Feed Controller** - Manages feed creation and updates
- **Authentication** - User identity and key management
- **Event Actions** - High-level operations like reacting, replying, etc.
- **Profile Management** - User profile handling and metadata
- **Relay Directories** - Discovery and management of relays
## Installation
```bash
npm install @welshman/app
```
+103
View File
@@ -0,0 +1,103 @@
# Router
The Router is the critical component to efficiently enable the `outbox model` in your Nostr application. It handles relay selection for reading, writing, and discovering events while considering relay quality, user preferences, and network conditions.
## Overview
The router provides scenarios for common **Nostr** operations:
- Reading user profiles
- Publishing events
- Following threads
- Handling DMs
- Searching content
Each scenario considers:
- User's relay preferences (NIP-65)
- Event hints in tags
- Relay quality scores
- Fallback policies
- Connection status
## Basic Usage
```typescript
import {ctx, setContext} from '@welshman/lib'
import {getDefaultAppContext} from '@welshman/app'
// Initialize router
setContext({
app: getDefaultAppContext()
})
// Use router scenarios
const router = ctx.app.router
// Get relays for reading a profile
const readRelays = router.ForPubkey(pubkey).getUrls()
// Get relays for publishing
const writeRelays = router.FromUser().getUrls()
// Get relays for a thread
const threadRelays = router.Replies(event).getUrls()
```
## Thread Navigation
```typescript
import {ctx} from '@welshman/lib'
import {createEvent, NOTE} from '@welshman/util'
import {publishThunk} from '@welshman/app'
const loadThread = async (event: TrustedEvent) => {
// Get relays for root event
const rootRelays = ctx.app.router
.EventRoots(event)
.getUrls()
// Get relays for replies
const replyRelays = ctx.app.router
.EventParents(event)
.getUrls()
// Get relays for mentions
const mentionRelays = ctx.app.router
.EventMentions(event)
.getUrls()
// Load from all relevant relays
await Promise.all([
subscribe({filters, relays: rootRelays}),
subscribe({filters, relays: replyRelays}),
subscribe({filters, relays: mentionRelays})
])
}
// Posting a reply
const reply = async (parent: TrustedEvent, content: string) => {
const event = createEvent(NOTE, {content})
// Get optimal relays for publishing
const relays = ctx.app.router
.PublishEvent(event)
// Skip .onion relays
.allowOnion(false)
// Allow up to 5 relays
.limit(5)
.getUrls()
return publishThunk({event, relays})
}
```
## Router Features
- Smart relay selection based on context
- Quality scoring of relays
- Fallback strategies
- Handling of special relay types (.onion, local)
- Automatic weight calculation
- Connection state awareness
- NIP-65 compliance
The router is central to efficient nostr operations, ensuring events reach their intended audience while minimizing unnecessary network traffic.
+186
View File
@@ -0,0 +1,186 @@
# Session Management
The session system provides a unified way to handle different authentication methods:
- Secret Key NIP-01
- Nostr Extensions NIP-07
- Bunker URL NIP-46
- Amber or in-device NIP-55
while managing user state and encryption capabilities.
## Overview
Sessions are stored in local storage and can be:
- Persisted across page reloads
- Used with multiple accounts
- Switched dynamically
- Backed by different signing methods
## Basic Usage
```typescript
import {ctx, setContext} from '@welshman/lib'
import {
getDefaultNetContext,
getDefaultAppContext,
pubkey,
sessions,
session,
addSession,
getNip07
} from '@welshman/app'
// Set up app config
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext(),
})
// Log in via NIP-07 extension (browser wallet)
if (await getNip07()) {
addSession({
method: 'nip07',
pubkey: await getNip07().getPublicKey()
})
}
// Get current session
console.log(session.get()) // Current active session
console.log(pubkey.get()) // Current pubkey
```
## Multiple Sessions
```typescript
import {sessions, pubkey, addSession, dropSession} from '@welshman/app'
// Add multiple sessions
addSession({method: 'nip07', pubkey: 'abc...'})
addSession({method: 'nip46', pubkey: 'def...', secret: '123'})
// Switch between sessions
pubkey.set('abc...') // Activates that session
// Remove a session
dropSession('abc...')
// List all sessions
console.log(sessions.get())
```
## NIP-46 (Bunker) Authentication
```typescript
import {Nip46Broker, Nip46Signer} from '@welshman/signer'
import {addSession} from '@welshman/app'
// Connect to a bunker
const clientSecret = makeSecret()
const relays = ['wss://relay.damus.io']
const broker = Nip46Broker.get({relays, clientSecret})
// Generate nostrconnect URL for the bunker
const connectUrl = await broker.makeNostrconnectUrl({
name: "My App",
url: "https://myapp.com"
})
// Wait for user to approve in bunker
const response = await broker.waitForNostrconnect(connectUrl)
// Create session
addSession({
method: 'nip46',
pubkey: response.event.pubkey,
secret: clientSecret,
handler: {
pubkey: response.event.pubkey,
relays
}
})
```
## Using Session Signer
```typescript
import {signer, session} from '@welshman/app'
import {createEvent, NOTE} from '@welshman/util'
// Current session's signer is always ready to use
const event = await signer.get().sign(
createEvent(NOTE, {content: "Hello Nostr!"})
)
// Encrypt content for private notes
const encrypted = await signer.get().nip44.encrypt(
pubkey,
"Secret message"
)
```
## Session Persistence
Sessions are automatically persisted to local storage. On page load:
```typescript
import {pubkey, sessions} from '@welshman/app'
// Sessions load automatically from local storage
console.log(sessions.get()) // All stored sessions
// the current active session
console.log(session.get())
// Last active pubkey is restored
console.log(pubkey.get())
```
## Session Types
```typescript
type SessionNip07 = {
method: "nip07"
pubkey: string
}
type SessionNip46 = {
method: "nip46"
pubkey: string
secret: string
handler: {
pubkey: string
relays: string[]
}
}
type SessionNip01 = {
method: "nip01"
pubkey: string
secret: string
}
```
## Error Handling
```typescript
import {tryCatch} from '@welshman/lib'
import {addSession, getNip07} from '@welshman/app'
const login = async () => {
const nip07 = await tryCatch(getNip07)
if (!nip07) {
throw new Error("No NIP-07 extension found")
}
const pubkey = await tryCatch(
() => nip07.getPublicKey()
)
if (!pubkey) {
throw new Error("Failed to get public key")
}
addSession({method: 'nip07', pubkey})
}
```
+68
View File
@@ -0,0 +1,68 @@
# Storage
The storage system provides IndexedDB persistence for stores and repositories.
It's critical to initialize this early in your application lifecycle to ensure data consistency.
```typescript
import {
initStorage,
storageAdapters,
throttled,
repository,
tracker,
relays,
handles,
freshness,
plaintext
} from '@welshman/app'
// Real world example from Coracle
const initializeStorage = async () => {
const ready = initStorage("coracle-db", 1, {
// Persist relay info
relays: {
keyPath: "url",
store: throttled(3000, relays)
},
// Persist NIP-05 handles
handles: {
keyPath: "nip05",
store: throttled(3000, handles)
},
// Track data freshness
freshness: storageAdapters.fromObjectStore(
freshness,
{throttle: 3000}
),
// Store decrypted content
plaintext: storageAdapters.fromObjectStore(
plaintext,
{throttle: 3000}
),
// Store events and their sources
events: storageAdapters.fromRepositoryAndTracker(
repository,
tracker,
{throttle: 3000}
)
})
// Wait for storage to be ready
await ready
// App can now start loading data
}
```
The storage system:
- Persists data across page reloads
- Throttles writes for performance
- Handles store migrations
- Syncs bidirectionally
- Supports custom adapters
Initialize storage before making any subscriptions or loading data to ensure proper caching behavior.
+108
View File
@@ -0,0 +1,108 @@
# Stores and Loaders
The `@welshman/app` package provides a powerful system of collection-based reactive stores and loader utilities.
These utilities follow a consistent pattern for working with various types of Nostr data, making it easy to:
1. Query data from the repository
2. Transform it into application-specific structures
3. Access it reactively in your UI
4. Trigger network loading when needed
## Core Concept
Each collection-based module exports a similar set of utilities:
```typescript
// Common pattern across collection-based modules
export const {
// Main collection store (derived from repository)
store: follows,
// Indexed map for efficient lookup
indexStore: followsByPubkey,
// Function to get a reactive store for a specific item
deriveItem: deriveFollows,
// Function to trigger loading an item from the network
loadItem: loadFollows
} = collection({
name: "collection-name",
store: baseStore,
getKey: item => item.keyProperty,
load: async (key) => { /* Loading logic */ }
})
```
## Available Collections
| Collection | Key | Kind | Description |
|------------|-----|------|-------------|
| `follows` | pubkey | 3 | User follow lists |
| `mutes` | pubkey | 10000 | User mute lists |
| `pins` | pubkey | 10001 | User pinned items |
| `profiles` | pubkey | 0 | User profile metadata |
| `relaySelections` | pubkey | 10002 | User relay preferences |
| `inboxRelaySelections` | pubkey | 10005 | User inbox relay settings |
| `zappers` | lnurl | - | Lightning zapper metadata |
| `handles` | nip05 | - | NIP-05 identifier metadata |
## Usage Examples
### Loading and Accessing Data
```typescript
import { loadProfile, deriveProfile, profilesByPubkey } from '@welshman/app'
// Trigger loading a profile from the network
await loadProfile('pubkey123')
// Get a reactive store for a specific profile
const profile = deriveProfile('pubkey123')
// Access all profiles by pubkey
const allProfiles = profilesByPubkey.get()
const specificProfile = allProfiles.get('pubkey123')
```
### User-Specific Collections
Several modules provide user-specific derived stores that automatically load data for the currently signed-in user:
```typescript
import { userProfile, userFollows, userMutes, userPins } from '@welshman/app'
// These are derived stores that automatically:
// 1. Watch for changes to the current user's pubkey
// 2. Load the appropriate data when the user changes
// 3. Provide the data reactively
userProfile.subscribe(profile => {
// Current user's profile data
})
userFollows.subscribe(follows => {
// Current user's follow list
})
```
### Web of Trust Utilities
The `wot.ts` module provides additional utilities for analyzing the social graph:
```typescript
import { getFollows, getFollowers, getNetwork, getWotScore } from '@welshman/app'
// Get users followed by a pubkey
const followedUsers = getFollows('pubkey123')
// Get users following a pubkey
const followers = getFollowers('pubkey123')
// Get extended network (follows-of-follows)
const network = getNetwork('pubkey123')
// Calculate trust score between users
const score = getWotScore('userPubkey', 'targetPubkey')
```
+153
View File
@@ -0,0 +1,153 @@
# Subscription System
The subscription system extends Nostr's base subscription model with intelligent caching, repository integration, and configurable behaviors.
## Key Concepts
- **Local Repository**: Events are automatically cached and tracked
- **Cache Intelligence**: Smart decisions about when to use cached data
- **Relay Integration**: Works with the router for optimal relay selection
- **Configurable Behavior**: Control caching and timeouts
## Configuration Options
```typescript
type SubscribeRequest = {
// Required
filters: Filter[] // What to query
// Behavior Control
closeOnEose?: boolean // Auto-close and use cache
timeout?: number // Max time to wait
authTimeout?: number // Time for auth negotiation
requestDelay?: number // Delay between batched requests
// Optional
relays?: string[] // Specific relays to query
// Event Handlers
onEvent?: (event: TrustedEvent) => void
onEose?: (url: string) => void
onComplete?: () => void
}
```
## Cache Behavior Control
The `closeOnEose` parameter is crucial for controlling caching behavior:
```typescript
// WITH closeOnEose: true (default for load())
// - Checks cache first
// - Returns cached results if complete
// - Closes after EOSE
// - Good for: Known events, historical data
const loadKnownEvent = async (id: string) => {
const events = await load({
filters: [{ids: [id]}],
closeOnEose: true
})
return events[0]
}
// WITH closeOnEose: false
// - Always queries relays
// - Stays open for updates
// - Ignores cache completeness
// - Good for: Replaceable events, live data
const watchProfile = (pubkey: string) => {
return subscribe({
filters: [{
kinds: [PROFILE],
authors: [pubkey]
}],
closeOnEose: false // Force relay query
})
}
```
## Common Usage Patterns
### One-time Queries
```typescript
// Load specific event
const event = await load({
filters: [{ids: [eventId]}]
// closeOnEose: true by default
})
// Load latest profile
const profile = await load({
filters: [{
kinds: [PROFILE],
authors: [pubkey],
limit: 1
}],
closeOnEose: false // Get latest from network
})
```
### Live Subscriptions
```typescript
// Watch for updates
const sub = subscribe({
filters: [{
kinds: [NOTE],
since: now() // Only new events
}],
closeOnEose: false, // Stay open
})
sub.on('event', (url, event) => {
// Handle live events
})
```
### Smart Caching
```typescript
// Profile loader with refresh control
const loadProfile = async (pubkey: string, options = {}) => {
const {
forceRefresh = false, // Skip cache
timeout = 3000, // Max wait time
relays = [] // Optional relay override
} = options
// Get optimal relays if not specified
const targetRelays = relays.length > 0
? relays
: ctx.app.router.ForPubkey(pubkey).getUrls()
return new Promise((resolve) => {
const sub = subscribe({
filters: [{
kinds: [PROFILE],
authors: [pubkey],
limit: 1
}],
relays: targetRelays,
closeOnEose: !forceRefresh, // Control cache behavior
timeout,
onEvent: (url, event) => {
resolve(event)
sub.close()
},
onComplete: () => resolve(null)
})
})
}
```
## Repository Integration
All events from subscriptions are automatically:
- Saved to the repository
- Tracked to their source relay
- Checked against deletion status
The repository serves as an intelligent cache layer, making subsequent queries for the same data faster.
+69
View File
@@ -0,0 +1,69 @@
# Tag Utilities
The tag utilities provide helper functions for creating properly formatted Nostr event tags with correct relay hints and metadata.
These are especially useful when creating events that reference other events or users.
## Tag Creators
### User Tags
```typescript
import {tagPubkey} from '@welshman/app'
// Create a p-tag with relay hint and profile name
const tag = tagPubkey(authorPubkey)
// => ["p", pubkey, "wss://relay.example.com", "username"]
```
### Event Reference Tags
```typescript
import {
tagEvent, // Basic event reference
tagEventForQuote, // For quoting events
tagEventForReply, // For reply threads
tagEventForComment, // For NIP-23 comments
tagEventForReaction // For reactions
} from '@welshman/app'
// Real world example: Creating a reply
const createReply = async (parent: TrustedEvent, content: string) => {
// Get proper tags for a reply, including:
// - All referenced pubkeys
// - Root/reply markers
// - Inherited mentions
// - Relay hints
const tags = tagEventForReply(parent)
const event = await signer.get().sign(
createEvent(NOTE, {
content,
tags,
created_at: now()
})
)
return publishThunk({
event,
// Use relay hints from tags
relays: ctx.app.router.PublishEvent(event).getUrls()
})
}
```
All tag creators:
- Add appropriate relay hints using the router
- Handle replaceable/parameterized events
- Follow adequate NIP-10/NIP-22 conventions for threading
- Include metadata like usernames
- Deduplicate references
- Preserve tag order
The tagging system is crucial for:
- Thread construction
- Event reactions
- User mentions
- Zap splits
Tag utilities ensure consistent and correct tag creation across the application while integrating with the router for relay hints.
+52
View File
@@ -0,0 +1,52 @@
# Thunks
Thunks provide optimistic updates for event publishing. They immediately update the local repository while handling the actual signing and publishing asynchronously, making the UI feel more responsive.
## Overview
A thunk:
- Updates local state immediately
- Handles event signing in the background
- Manages publish status per relay
- Supports soft-undo via abort
- Can be delayed/cancelled
- Tracks successful publishes
## Basic Usage
```typescript
import {publishThunk} from '@welshman/app'
import {createEvent, NOTE} from '@welshman/util'
const publish = async (content: string) => {
// Get optimal relays for publishing
const relays = ctx.app.router
.FromUser()
.getUrls()
// Create and publish thunk
const thunk = await publishThunk({
event: createEvent(NOTE, {content}),
relays,
delay: 3000, // 3s window for abort
})
// Track publish status
thunk.status.subscribe(statuses => {
for (const [url, {status, message}] of Object.entries(statuses)) {
console.log(`${url}: ${status} ${message}`)
}
})
// Can abort within delay window
setTimeout(() => {
if (userWantsToCancel) {
thunk.controller.abort()
}
}, 1000)
// Wait for completion
const results = await thunk.result
return results
}
```
+39
View File
@@ -0,0 +1,39 @@
# Topics
The topics system provides a reactive way to track and count hashtags across all events in the repository. It automatically updates as new events arrive or are removed.
```typescript
import {topics} from '@welshman/app'
// In a Svelte component
<script>
// Reactive list of all topics with counts
$: topicList = $topics
.sort((a, b) => b.count - a.count)
.slice(0, 20)
</script>
<div class="topics">
{#each topicList as {name, count}}
<a href="/t/{name}">
#{name}
<span class="count">({count})</span>
</a>
{/each}
</div>
```
The store:
- Updates automatically with new events
- Maintains topic counts
- Is throttled to prevent excess updates
- Is case-insensitive
- Integrates with the repository
Think of it as a live tag cloud that stays in sync with your local event cache.
This is commonly used for:
- Tag clouds
- Topic discovery
- Content organization
- Trending topics
+63
View File
@@ -0,0 +1,63 @@
# Web of Trust (WOT) Module
The `wot.ts` module provides utilities for implementing a Web of Trust system within Nostr applications. This system analyzes social connections (follows and mutes) to build a reputation graph that can be used for content filtering, user scoring, and discovery.
## Core Concepts
- **Follow Trust**: Users gain positive reputation when followed by those in your network
- **Mute Distrust**: Users lose reputation when muted by those in your network
- **WOT Graph**: A reactive weighted directed graph representing trust relationships
- **Contextual Scoring**: Reputation scores that adapt based on user's social graph
## API Reference
### Social Graph Navigation
```typescript
// Get users followed by a specific pubkey
getFollows(pubkey: string): string[]
// Get users who have muted a specific pubkey
getMutes(pubkey: string): string[]
// Get followers of a specific pubkey
getFollowers(pubkey: string): string[]
// Get users who have muted a specific pubkey
getMuters(pubkey: string): string[]
// Get the extended network (follows-of-follows) for a pubkey
getNetwork(pubkey: string): string[]
```
### Trust Analysis
```typescript
// Get follows of a user who also follow a target
getFollowsWhoFollow(pubkey: string, target: string): string[]
// Get follows of a user who have muted a target
getFollowsWhoMute(pubkey: string, target: string): string[]
// Calculate trust score between users
getWotScore(pubkey: string, target: string): number
```
### Reactive Stores
```typescript
// Map of follower lists by pubkey
followersByPubkey: Readable<Map<string, Set<string>>>
// Map of muter lists by pubkey
mutersByPubkey: Readable<Map<string, Set<string>>>
// The full WOT graph with scores (pubkey → score)
wotGraph: Readable<Map<string, number>>
// The maximum WOT score in the graph
maxWot: Readable<number>
// Derive the WOT score for a specific user
deriveUserWotScore(targetPubkey: string): Readable<number>
```