Delete readmes, update docs
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,88 +0,0 @@
|
||||
# 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.
|
||||
+6
-132
@@ -1,139 +1,13 @@
|
||||
# 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).
|
||||
The `@welshman/app` package uses a global context system to configure a few core behaviors.
|
||||
|
||||
## Basic Setup
|
||||
## Dufflepud
|
||||
|
||||
[Dufflepud](https://github.com/coracle-social/dufflepud) is a utility server that can retrieve NIP 05 profiles, zappers, relay metadata, link previews, etc. It's not necessary for using welshman, but can improve things by bypassing CORS.
|
||||
|
||||
```typescript
|
||||
import {ctx, setContext} from '@welshman/lib'
|
||||
import {getDefaultNetContext, getDefaultAppContext} from '@welshman/app'
|
||||
import {appContext} 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'
|
||||
appContext.dufflepudUrl = 'https://my-dufflepud-instance.com'
|
||||
```
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# 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.
|
||||
+63
-1
@@ -1,5 +1,7 @@
|
||||
# @welshman/app
|
||||
|
||||
[](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.
|
||||
|
||||
## What's Included
|
||||
@@ -7,11 +9,71 @@ A comprehensive framework for building nostr clients, powering production applic
|
||||
- **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
|
||||
- **Session Management** - 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
|
||||
- **Web of Trust** - Utilities for building webs of trust
|
||||
|
||||
## Quick Example
|
||||
|
||||
```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'
|
||||
|
||||
// Log in via NIP 07
|
||||
addSession({method: 'nip07', pubkey: await getNip07().getPubkey()})
|
||||
|
||||
// Enable automatic authentication to relays
|
||||
defaultSocketPolicies.push(
|
||||
makeSocketPolicyAuth({
|
||||
sign: (event: StampedEvent) => signer.get()?.sign(event),
|
||||
shouldAuth: (socket: Socket) => true,
|
||||
}),
|
||||
)
|
||||
|
||||
// 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, relaySelections, relays, follows, mutes.
|
||||
const profile = deriveProfile(pubkey.get())
|
||||
|
||||
// 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(),
|
||||
event: makeEvent(NOTE, {content: "hi"}),
|
||||
delay: 3000,
|
||||
})
|
||||
|
||||
// Thunks can be aborted until after `delay`, allowing for soft-undo
|
||||
thunk.controller.abort()
|
||||
|
||||
// Some commands are included
|
||||
const thunk = follow('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 req = request({
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
}])
|
||||
|
||||
// Listen for events
|
||||
req.on(RequestEvent.Event, (event: TrustedEvent) => {
|
||||
console.log(event)
|
||||
})
|
||||
|
||||
// Close the req
|
||||
req.close()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Making Requests
|
||||
|
||||
Welshman 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
|
||||
|
||||
## Request and Load
|
||||
|
||||
The base functionality for subscription management is implemented in `@welshman/net`. Please refer to [the documentation](/net) for that module for details.
|
||||
|
||||
## Collections and Loaders
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Deriving Events
|
||||
|
||||
The best way to create collections is by deriving their contents from the app `repository` using `deriveEvents` from `@welshman/store`. For more control, use `deriveEventsMapped`.
|
||||
|
||||
```typescript
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
|
||||
export const notes = deriveEvents<TrustedEvent>(repository, {filters: [{kinds: [NOTE]}]})
|
||||
```
|
||||
|
||||
A collection could then be created by passing the `notes` store to `collection`.
|
||||
|
||||
### Available Collections
|
||||
|
||||
Several common collections are built-in and ready for use:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Example - Loading and Displaying Profiles
|
||||
|
||||
```typescript
|
||||
import {get} from 'svelte/store'
|
||||
import {displayProfile} from '@welshman/util'
|
||||
import {deriveProfile, deriveProfileDisplay} from '@welshman/app'
|
||||
|
||||
// Subscribe to profile changes - this will automatically load the profile in the background
|
||||
const profile = deriveProfile(pubkey)
|
||||
|
||||
// Display with fallback
|
||||
const name = displayProfile(get(profile), 'unknown')
|
||||
|
||||
// Better: use built-in deriveProfileDisplay utility
|
||||
const name = deriveProfileDisplay(pubkey)
|
||||
```
|
||||
|
||||
### 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'
|
||||
|
||||
userProfile.subscribe(profile => {
|
||||
// Current user's profile data
|
||||
})
|
||||
|
||||
userFollows.subscribe(follows => {
|
||||
// Current user's follow list
|
||||
})
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
## Feeds
|
||||
|
||||
A high-level feed loader utility is also provided, which combines application state with utilities from `@welshman/net` and `@welshman/feeds`.
|
||||
|
||||
```typescript
|
||||
import {NOTE} from '@welshman/util'
|
||||
import {makeKindFeed} from '@welshman/feeds'
|
||||
import {createFeedController} from '@welshman/app'
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
let done = false
|
||||
|
||||
const ctrl = createFeedController({
|
||||
feed: makeKindFeed(NOTE),
|
||||
useWindowing: true,
|
||||
signal: abortController.signal,
|
||||
onEvent: e => {
|
||||
console.log(e)
|
||||
},
|
||||
onExhausted: () => {
|
||||
done = true
|
||||
},
|
||||
})
|
||||
|
||||
// Load some notes
|
||||
ctrl.load(100)
|
||||
|
||||
// Cancel any pending requests
|
||||
abortController.abort()
|
||||
```
|
||||
@@ -5,9 +5,10 @@ Thunks provide optimistic updates for event publishing. They immediately update
|
||||
## Overview
|
||||
|
||||
A thunk:
|
||||
|
||||
- Updates local state immediately
|
||||
- Handles event signing in the background
|
||||
- Manages publish status per relay
|
||||
- Handles event signing in the background using the current session
|
||||
- Tracks publish status per relay
|
||||
- Supports soft-undo via abort
|
||||
- Can be delayed/cancelled
|
||||
- Tracks successful publishes
|
||||
@@ -46,7 +47,17 @@ const publish = async (content: string) => {
|
||||
}, 1000)
|
||||
|
||||
// Wait for completion
|
||||
const results = await thunk.result
|
||||
return results
|
||||
await thunk.result
|
||||
}
|
||||
```
|
||||
|
||||
## Built in commands
|
||||
|
||||
Several thunk factories are provided for more complicated scenarios like updating lists:
|
||||
|
||||
- `follow(pubkey)`
|
||||
- `unfollow(pubkey)`
|
||||
- `mute(pubkey)`
|
||||
- `unmute(pubkey)`
|
||||
- `pin(tag)`
|
||||
- `unpin(tag)`
|
||||
@@ -0,0 +1,56 @@
|
||||
# Router
|
||||
|
||||
The Welshman router can be used to 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 {routerContext, addMaximalFallbacks, Router} from '@welshman/app'
|
||||
|
||||
// Set up global router options
|
||||
routerContext.getDefaultRelays = () => ["wss://relay.damus.io/", "wss://nos.lol/"]
|
||||
|
||||
// Router can be used directly with options, or via a singleton with global options
|
||||
const router = Router.get()
|
||||
|
||||
// Get relays for reading a profile
|
||||
const readRelays = router.ForPubkey(pubkey).getUrls()
|
||||
|
||||
// Get relays for broadcasting events by the current user
|
||||
const writeRelays = router.FromUser().getUrls()
|
||||
|
||||
// Get relays for a quote
|
||||
const quoteRelays = Router.get()
|
||||
.Quote(parentEvent, idOrAddress, relayHints)
|
||||
.policy(addMaximalFallbacks)
|
||||
.getUrls()
|
||||
|
||||
```
|
||||
|
||||
## Router Features
|
||||
|
||||
- Smart relay selection based on relay monitoring
|
||||
- Quality scoring of relays
|
||||
- Fallback strategies
|
||||
- Handling of special relay types (.onion, local)
|
||||
- NIP-65 support
|
||||
|
||||
The router is central to efficient nostr operations, ensuring events reach their intended audience while minimizing unnecessary network traffic.
|
||||
@@ -1,103 +0,0 @@
|
||||
# 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.
|
||||
+5
-80
@@ -1,16 +1,16 @@
|
||||
# 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.
|
||||
- NIP-01 via Secret Key
|
||||
- NIP-07 via Browser Extension
|
||||
- NIP-46 via Bunker URL or Nostrconnect
|
||||
- NIP-55 via Android Signer Application
|
||||
|
||||
## Overview
|
||||
|
||||
Sessions are stored in local storage and can be:
|
||||
|
||||
- Persisted across page reloads
|
||||
- Used with multiple accounts
|
||||
- Switched dynamically
|
||||
@@ -18,39 +18,6 @@ Sessions are stored in local storage and can be:
|
||||
|
||||
## 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'
|
||||
|
||||
@@ -118,23 +85,6 @@ const encrypted = await signer.get().nip44.encrypt(
|
||||
)
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -159,28 +109,3 @@ type SessionNip01 = {
|
||||
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})
|
||||
}
|
||||
```
|
||||
|
||||
+16
-52
@@ -1,67 +1,31 @@
|
||||
# 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.
|
||||
|
||||
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'
|
||||
import {initStorage, defaultStorageAdapters} 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)
|
||||
// Use default storage adapters, which track important metadata events,
|
||||
// relays, handles, zappers, etc.
|
||||
await initStorage("my-db", 1, {
|
||||
...defaultStorageAdapters,
|
||||
custom: {
|
||||
keyPath: "key",
|
||||
init: async () => console.log(await getAll("custom")),
|
||||
sync: () => {
|
||||
// Set up a listener for changes, using bulkPut to save records.
|
||||
// Return an unsubscribe function for cleanup
|
||||
},
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# 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')
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# 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.
|
||||
+5
-28
@@ -1,12 +1,13 @@
|
||||
# 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
|
||||
|
||||
### Pubkey Tags
|
||||
|
||||
### User Tags
|
||||
```typescript
|
||||
import {tagPubkey} from '@welshman/app'
|
||||
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
tagEvent, // Basic event reference
|
||||
tagEventForQuote, // For quoting events
|
||||
tagEventForReply, // For reply threads
|
||||
tagEventForComment, // For NIP-23 comments
|
||||
tagEventForComment, // For NIP-22 comments
|
||||
tagEventForReaction // For reactions
|
||||
} from '@welshman/app'
|
||||
|
||||
@@ -36,34 +37,10 @@ const createReply = async (parent: TrustedEvent, content: string) => {
|
||||
// - 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()
|
||||
relays: Router.get().PublishEvent(event).getUrls()
|
||||
event: await signer.get().sign(createEvent(NOTE, {content, tags})),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Web of Trust (WOT) Module
|
||||
# Web of Trust (WOT)
|
||||
|
||||
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.
|
||||
Welshman 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user