Delete readmes, update docs

This commit is contained in:
Jon Staab
2025-04-09 11:17:12 -07:00
parent 3301616e7d
commit 8e585fc150
53 changed files with 1946 additions and 2468 deletions
-103
View File
@@ -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.
-88
View File
@@ -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
View File
@@ -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'
```
-96
View File
@@ -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
View File
@@ -1,5 +1,7 @@
# @welshman/app
[![version](https://badgen.net/npm/v/@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
+143
View File
@@ -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)`
+56
View File
@@ -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.
-103
View File
@@ -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
View File
@@ -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
View File
@@ -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
-108
View File
@@ -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')
```
-153
View File
@@ -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
View File
@@ -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.
-39
View File
@@ -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
View File
@@ -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