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
+2 -1
View File
@@ -1,4 +1,5 @@
--ignore-dir=docs
--ignore-dir=docs/reference
--ignore-dir=docs/.vitepress/cache
--ignore-dir=dist
--ignore-dir=build
--ignore-dir=.svelte-kit
+2
View File
@@ -1,2 +1,4 @@
node_modules
docs/reference
docs/.vitepress/cache
build
+62 -60
View File
@@ -21,13 +21,17 @@ export default defineConfig({
],
},
{
text: "@welshman/lib",
link: "/lib/",
text: "@welshman/app",
link: "/app/",
items: [
{text: "Utilities", link: "/lib/tools"},
{text: "LRU cache", link: "/lib/lru"},
{text: "Worker", link: "/lib/worker"},
{text: "Deferred", link: "/lib/deferred"},
{text: "Session Management", link: "/app/session"},
{text: "Relay Selection", link: "/app/relay-selection"},
{text: "Making Requests", link: "/app/making-requests"},
{text: "Publishing Events", link: "/app/publishing-events"},
{text: "Tag utilities", link: "/app/tags"},
{text: "Web of Trust", link: "/app/wot"},
{text: "Storage", link: "/app/storage"},
{text: "Context", link: "/app/context"},
],
},
{
@@ -48,37 +52,6 @@ export default defineConfig({
{text: "Zaps", link: "/util/zaps"},
],
},
{
text: "@welshman/content",
link: "/content/",
items: [
{text: "Parser", link: "/content/parser"},
{text: "Renderer", link: "/content/renderer"},
],
},
{
text: "@welshman/feeds",
link: "/feeds/",
items: [
{text: "Core", link: "/feeds/core"},
{text: "Utilities", link: "/feeds/utils"},
{text: "Compiler", link: "/feeds/compiler"},
{text: "Controller", link: "/feeds/controller"},
],
},
{
text: "@welshman/editor",
link: "/editor/",
items: [],
},
{
text: "@welshman/store",
link: "/store/",
items: [
{text: "Basic utilities", link: "/store/basic"},
{text: "Event stores", link: "/store/events"},
],
},
{
text: "@welshman/net",
link: "/net/",
@@ -95,14 +68,6 @@ export default defineConfig({
{text: "Socket", link: "/net/socket"},
],
},
{
text: "@welshman/dvm",
link: "/dvm/",
items: [
{text: "Handler", link: "/dvm/handler"},
{text: "Request", link: "/dvm/request"},
],
},
{
text: "@welshman/signer",
link: "/signer/",
@@ -116,22 +81,59 @@ export default defineConfig({
],
},
{
text: "@welshman/app",
link: "/app/",
text: "@welshman/relay",
link: "/relay/",
items: [
{text: "Context", link: "/app/context"},
{text: "Storage", link: "/app/storage"},
{text: "Router", link: "/app/router"},
{text: "Session", link: "/app/session"},
{text: "Collection", link: "/app/collection"},
{text: "Commands", link: "/app/commands"},
{text: "Subscription", link: "/app/subscription"},
{text: "Publish (Thunks)", link: "/app/thunks"},
{text: "Feed", link: "/app/feed"},
{text: "Tag utilities", link: "/app/tags"},
{text: "Topics", link: "/app/topics"},
{text: "Web of Trust", link: "/app/wot"},
{text: "Stores and Loaders", link: "/app/storesandloaders"},
],
},
{
text: "@welshman/content",
link: "/content/",
items: [
{text: "Parser", link: "/content/parser"},
{text: "Renderer", link: "/content/renderer"},
],
},
{
text: "@welshman/editor",
link: "/editor/",
items: [],
},
{
text: "@welshman/feeds",
link: "/feeds/",
items: [
{text: "Core", link: "/feeds/core"},
{text: "Utilities", link: "/feeds/utils"},
{text: "Compiler", link: "/feeds/compiler"},
{text: "Controller", link: "/feeds/controller"},
],
},
{
text: "@welshman/dvm",
link: "/dvm/",
items: [
{text: "Handler", link: "/dvm/handler"},
{text: "Request", link: "/dvm/request"},
],
},
{
text: "@welshman/store",
link: "/store/",
items: [
{text: "Basic utilities", link: "/store/basic"},
{text: "Event stores", link: "/store/events"},
],
},
{
text: "@welshman/lib",
link: "/lib/",
items: [
{text: "Utilities", link: "/lib/tools"},
{text: "LRU cache", link: "/lib/lru"},
{text: "Task Queue", link: "/lib/task-queue"},
{text: "Normalize URL", link: "/lib/normalize-url"},
{text: "Deferred", link: "/lib/deferred"},
],
},
],
-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
+2
View File
@@ -1,5 +1,7 @@
# @welshman/content
[![version](https://badgen.net/npm/v/@welshman/content)](https://npmjs.com/package/@welshman/content)
`@welshman/content` is a comprehensive content processing library designed specifically for Nostr applications.
It provides a robust system for parsing, processing, and rendering Nostr content while handling various special formats and entities common in the Nostr ecosystem.
+86
View File
@@ -1,5 +1,7 @@
# @welshman/dvm
[![version](https://badgen.net/npm/v/@welshman/dvm)](https://npmjs.com/package/@welshman/dvm)
`@welshman/dvm` is a comprehensive package for building and interacting with Data Vending Machines (DVMs) in the Nostr ecosystem. It provides both server-side DVM implementation capabilities and client-side request handling.
## What is a DVM?
@@ -10,6 +12,90 @@ A Data Vending Machine (DVM) is a Nostr service that:
- Responds with new events containing processed data
- Optionally provides progress updates during processing
# Request example
```typescript
import {makeDvmRequest, DVMEvent} from '@welshman/dvm'
const req = makeDvmRequest({
// Create and sign a dvm request event, including any desired tags
event: createAndSign({kind: 5300}),
// Publish and subscribe to these relays
relays: ['wss://relay.damus.io', 'wss://dvms.f7z.io'],
// Timeout defaults to 30 seconds
timeout: 30_000,
// Auto close on first result (defaults to true)
autoClose: true,
// Listen for and emit `progress` events
reportProgress: true,
})
// Listen for progress, result, etc
req.emitter.on(DVMEvent.Progress, (url, event) => console.log(event))
req.emitter.on(DVMEvent.Result, (url, event) => console.log(event))
```
# Handler example
```typescript
import {bytesToHex} from '@noble/hashes/utils'
import {generateSecretKey} from 'nostr-tools'
import {createEvent} from '@welshman/util'
import {subscribe} from '@welshman/net'
import {DVM} from '@welshman/dvm'
// Your DVM's private key. Store this somewhere safe
// const hexPrivateKey = bytesToHex(generateSecretKey())
const hexPrivateKey = '9cd387a3aa0c1abc2ef517c8402f29c069b4174e02a426491aec7566501bee67'
// Tags that we'll return as content discovery suggestions
const tags = []
// Populate the tags with music by Ainsley Costello
const sub = subscribe({
timeout: 30_000,
relays: ["wss://relay.wavlake.com"],
filters: [{
kinds: [31337],
'#p': ['8806372af51515bf4aef807291b96487ea1826c966a5596bca86697b5d8b23bc'],
}],
})
// Push event ids to our suggestions
sub.on('event', (url, e) => tags.push(["e", e.id, url]))
const dvm = new DVM({
// The private key used to sign events
sk: hexPrivateKey,
// Relays that the DVM will listen on
relays: ['wss://relay.damus.io', 'wss://dvms.f7z.io'],
// Only listen to requests tagging our dvm
requireMention: true,
// Expire results after 1 hour (the default)
expireAfter: 60 * 60,
// Handlers for various kinds
handlers: {
5300: dvm => ({
handleEvent: async function* (event) {
// DVM responses are stringified into the content
const content = JSON.stringify(tags)
// Yield our response. Kind 7000 can be used for partial results too
yield createEvent(event.kind + 1000, {content})
},
}),
}
})
// Enable logging
dvm.logEvents = true
// When you're ready
dvm.start()
// When you're done
dvm.stop()
```
## Installation
+2
View File
@@ -1,5 +1,7 @@
# @welshman/editor
[![version](https://badgen.net/npm/v/@welshman/editor)](https://npmjs.com/package/@welshman/editor)
`@welshman/editor` provides a comprehensive Nostr-ready text editor, built on top of [nostr-editor](https://github.com/cesardeazevedo/nostr-editor).
This package powers the editors of [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social).
+35 -2
View File
@@ -1,7 +1,10 @@
# @welshman/feeds
A powerful package for building and executing dynamic Nostr feeds.
It provides a declarative way to define complex feed compositions using set operations (union, intersection, difference) and various filtering mechanisms.
[![version](https://badgen.net/npm/v/@welshman/feeds)](https://npmjs.com/package/@welshman/feeds)
A package for building and executing dynamic Nostr feeds which provides a declarative way to define complex feed compositions using set operations (union, intersection, difference) and various filtering mechanisms.
Read the spec on [the NIPs repository](https://github.com/nostr-protocol/nips/blob/af4329986cae9b0ef625a01c8cefd5e802ca6895/fe.md).
## What's Included
@@ -11,6 +14,36 @@ It provides a declarative way to define complex feed compositions using set oper
- **Feed Utils** - Helper functions for creating and manipulating feeds
- **Feed Types** - Supports authors, kinds, tags, DVMs, lists, WOT, and more
## Quick Example
```javascript
// Define a feed using set operations
const feed = intersectionFeed(
unionFeed(
dvmFeed({
kind: 5300,
pubkey: '19b78ccfa7c5e31e6bacbb3f2a1703f64b62017702e584440bf29a7e16263e8c',
}),
listFeed("10003:19ba654f26afd4930fd3d51baf4e26f1413b7aeec7190cd6c0cdf4d2f14cec6b:"),
)
wotFeed({min: 0.1}),
scopeFeed("global"),
)
// Create a controller, providing required context via FeedOptions
const controller = new FeedController({
feed,
request,
requestDVM,
getPubkeysForScope,
getPubkeysForWOTRange,
onEvent: event => console.log("Event", event),
onExhausted: () => console.log("Exhausted"),
})
// Load notes using the feed
const events = await controller.load(10)
```
## Installation
+8 -8
View File
@@ -13,23 +13,23 @@ npm i @welshman/relay
# Networking and relay management
npm i @welshman/net
# Content parsing and rendering
npm i @welshman/content
# Event signing and encryption
npm i @welshman/signer
# Dynamic feed compilation
npm i @welshman/feeds
# Content parsing and rendering
npm i @welshman/content
# Rich text editor component
npm i @welshman/editor
# Svelte stores and state management
npm i @welshman/store
# Full application framework (requires Svelte)
# Complete application framework
npm i @welshman/app
# Rich text editor component (requires Svelte)
npm i @welshman/editor
```
Choose packages based on your needs:
@@ -49,4 +49,4 @@ Choose packages based on your needs:
npm i @welshman/content
```
Each package is independent but integrates seamlessly. All packages are framework-agnostic, but work best with Svelte.
Each package is independent but integrates seamlessly. All packages are framework-agnostic, but work best with Svelte due to svelte stores being a common pattern for state management.
+1 -1
View File
@@ -1,6 +1,6 @@
# Deferred Promises
The Deferred module provides utilities for creating promises with exposed resolve/reject functions and typed error handling. This is particularly useful for managing asynchronous operations where you need external control over promise resolution.
The `Deferred` module provides utilities for creating promises with exposed resolve/reject functions and typed error handling. This is particularly useful for managing asynchronous operations where you need external control over promise resolution.
## Types
+2
View File
@@ -1,5 +1,7 @@
# @welshman/lib
[![version](https://badgen.net/npm/v/@welshman/lib)](https://npmjs.com/package/@welshman/lib)
A lightweight TypeScript utility library with zero dependencies, providing essential tools for modern JavaScript development.
## What's Included
+1 -1
View File
@@ -1,6 +1,6 @@
# LRU Cache
The LRU (Least Recently Used) Cache implementation provides efficient caching with automatic eviction of least recently used items when the cache reaches its maximum size.
A LRU (Least Recently Used) Cache implementation provides efficient caching with automatic eviction of least recently used items when the cache reaches its maximum size.
## Basic Usage
+69
View File
@@ -0,0 +1,69 @@
# URL Normalization
A `normalizeUrl` function borrowed from [sindresorhus/normalize-url](https://github.com/sindresorhus/normalize-url) is included for convenience.
## Basic Usage
```typescript
normalizeUrl('example')
//=> 'http://example'
normalizeUrl('sindresorhus.com/about.html#contact', {stripHash: true});
//=> 'http://sindresorhus.com/about.html'
```
## API Reference
### Configuration
```typescript
export type Options = {
// Default protocol to prepend
readonly defaultProtocol?: 'https' | 'http'
// Prepends `defaultProtocol` to the URL if it's protocol-relative.
readonly normalizeProtocol?: boolean
// Normalizes HTTPS URLs to HTTP.
readonly forceHttp?: boolean
// Normalizes HTTP URLs to HTTPS.
readonly forceHttps?: boolean
// Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL.
readonly stripAuthentication?: boolean
// Removes hash from the URL.
readonly stripHash?: boolean
// Remove the protocol from the URL: `http://sindresorhus.com` → `sindresorhus.com`.
readonly stripProtocol?: boolean
// Strip the [text fragment](https://web.dev/text-fragments/) part of the URL
readonly stripTextFragment?: boolean
// Removes `www.` from the URL.
readonly stripWWW?: boolean
// Removes query parameters that matches any of the provided strings or regexes.
readonly removeQueryParameters?: ReadonlyArray<RegExp | string> | boolean
// Keeps only query parameters that matches any of the provided strings or regexes.
readonly keepQueryParameters?: ReadonlyArray<RegExp | string>
// Removes trailing slash.
readonly removeTrailingSlash?: boolean
// Remove a sole `/` pathname in the output. This option is independent of `removeTrailingSlash`.
readonly removeSingleSlash?: boolean
// Removes the default directory index file from path that matches any of the provided strings or regexes.
readonly removeDirectoryIndex?: boolean | ReadonlyArray<RegExp | string>
// Removes an explicit port number from the URL.
readonly removeExplicitPort?: boolean
// Sorts the query parameters alphabetically by key.
readonly sortQueryParameters?: boolean
}
```
+59
View File
@@ -0,0 +1,59 @@
# Task Queue
The `TaskQueue` class provides a simple queue processing system with batched operations and throttling. It's designed to handle asynchronous operations efficiently while maintaining control over processing rates and resource usage.
## Basic Usage
```typescript
// Create queue for processing messages
const queue = new TaskQueue<number>({
chunkSize: 10,
processItem: (n: number) => console.log(n)
})
// Add and remove items to/from queue
worker.push(10)
worker.push(21)
worker.remove(10)
worker.push(57)
```
## Control Methods
Control message processing:
```typescript
// Pause processing
worker.stop()
// Resume processing
worker.start()
// Clear queue
worker.clear()
```
## API Reference
### Constructor
```typescript
class TaskQueue<Item> {
constructor(readonly options: TaskQueueOptions<Item>) {}
}
```
The TaskQueue class accepts messages of type `Item` and processes them.
### Configuration
```typescript
type TaskQueueOptions<Item> = {
// How many items to process at a time
batchSize: number
// A function for processing items. Any promises returned will be awaited
processItem: (item: Item) => unknown
}
```
+367 -174
View File
@@ -1,256 +1,449 @@
# Utility Functions
The `Tools.ts` module provides a comprehensive collection of utility functions for common programming tasks. It includes functions for array manipulation, object handling, type checking, math operations, and more.
The `Tools` module provides a comprehensive collection of utility functions for common programming tasks. It includes functions for array manipulation, object handling, type checking, math operations, and more.
## Types
## Basic functional programming utilities
```typescript
type Nil = null | undefined
type Maybe<T> = T | undefined
type Obj<T = any> = Record<string, T>
// Function that does nothing and returns undefined
export declare const noop: (...args: unknown[]) => undefined;
// Returns the input value unchanged
export declare const identity: <T>(x: T, ...args: unknown[]) => T;
// Creates a function that always returns the same value
export declare const always: <T>(x: T, ...args: unknown[]) => () => T;
// Returns the logical NOT of a value
export declare const not: (x: any, ...args: unknown[]) => boolean;
// Deep equality comparison
export declare const equals: (a: any, b: any) => boolean;
```
## Categories
### Type Checking & Basic Operations
## Numbers
```typescript
// Check if value is null or undefined
isNil(x: any): boolean
// Converts string or number to number
export declare const ensureNumber: (x: number | string) => number;
// Execute function if value exists
ifLet<T>(x: T | undefined, f: (x: T) => void)
// Converts a `number | undefined` to a number, defaulting to 0
export declare const num: (x: number | undefined) => number;
// Return value unchanged
identity<T>(x: T): T
// Adds two numbers, handling undefined values
export declare const add: (x: number | undefined, y: number | undefined) => number;
// Create function that always returns same value
always<T>(x: T): () => T
// Subtracts two numbers, handling undefined values
export declare const sub: (x: number | undefined, y: number | undefined) => number;
// Logical NOT
not(x: any): boolean
// Multiplies two numbers, handling undefined values
export declare const mul: (x: number | undefined, y: number | undefined) => number;
// Create complement of a predicate function
complement<T extends unknown[]>(f: (...args: T) => any): (...args: T) => boolean
// Divides two numbers, handling undefined values
export declare const div: (x: number | undefined, y: number) => number;
// Increments a number by 1, handling undefined values
export declare const inc: (x: number | undefined) => number;
// Decrements a number by 1, handling undefined values
export declare const dec: (x: number | undefined) => number;
// Less than comparison, handling undefined values
export declare const lt: (x: number | undefined, y: number | undefined) => boolean;
// Less than or equal comparison, handling undefined values
export declare const lte: (x: number | undefined, y: number | undefined) => boolean;
// Greater than comparison, handling undefined values
export declare const gt: (x: number | undefined, y: number | undefined) => boolean;
// Greater than or equal comparison, handling undefined values
export declare const gte: (x: number | undefined, y: number | undefined) => boolean;
// Returns maximum value in array, handling undefined values
export declare const max: (xs: (number | undefined)[]) => number;
// Returns minimum value in array, handling undefined values
export declare const min: (xs: (number | undefined)[]) => number;
// Returns sum of array values, handling undefined values
export declare const sum: (xs: (number | undefined)[]) => number;
// Returns average of array values, handling undefined values
export declare const avg: (xs: (number | undefined)[]) => number;
// Checks if a number is between two values (exclusive)
export declare const between: ([low, high]: [number, number], n: number) => boolean;
// Checks if a number is between two values (inclusive)
export declare const within: ([low, high]: [number, number], n: number) => boolean;
// Constrains number between min and max values
export declare const clamp: ([min, max]: [number, number], n: number) => number;
// Round a number to the nearest float precision
export declare const round: (precision: number, x: number) => number;
```
### Array Operations
## Timestamps
```typescript
// Get first element
first<T>(xs: T[]): T | undefined
// One minute in seconds
export declare const MINUTE = 60;
// Get first element of first array
ffirst<T>(xs: T[][]): T | undefined
// One hour in seconds
export declare const HOUR: number;
// Get last element
last<T>(xs: T[]): T | undefined
// One day in seconds
export declare const DAY: number;
// Drop first n elements
drop<T>(n: number, xs: T[]): T[]
// One week in seconds
export declare const WEEK: number;
// Take first n elements
take<T>(n: number, xs: T[]): T[]
// One month in seconds (approximate)
export declare const MONTH: number;
// Remove duplicates
uniq<T>(xs: T[]): T[]
// One quarter in seconds (approximate)
export declare const QUARTER: number;
// Remove duplicates by key function
uniqBy<T>(f: (x: T) => any, xs: T[]): T[]
// One year in seconds (approximate)
export declare const YEAR: number;
// Create array of n items using generator function
initArray<T>(n: number, f: () => T): T[]
// Multiplies time unit by count
export declare const int: (unit: number, count?: number) => number;
// Split array into chunks
chunk<T>(chunkLength: number, xs: T[]): T[][]
// Returns current Unix timestamp in seconds
export declare const now: () => number;
// Split array into n chunks
chunks<T>(n: number, xs: T[]): T[][]
// Returns Unix timestamp from specified time ago
export declare const ago: (unit: number, count?: number) => number;
// Converts seconds to milliseconds
export declare const ms: (seconds: number) => number;
```
### Object Operations
## Sequences
```typescript
// Create object excluding specified keys
omit<T extends Obj>(ks: string[], x: T): T
// Returns the first element of an array
export declare const first: <T>(xs: Iterable<T>, ...args: unknown[]) => T | undefined;
// Create object excluding entries with specified values
omitVals<T extends Obj>(xs: any[], x: T): T
// Returns the first element of the first array in a nested array
export declare const ffirst: <T>(xs: Iterable<Iterable<T>>, ...args: unknown[]) => T | undefined;
// Create object with only specified keys
pick<T extends Obj>(ks: string[], x: T): T
// Returns the last element of an array
export declare const last: <T>(xs: Iterable<T>, ...args: unknown[]) => T;
// Transform object keys
mapKeys<T extends Obj>(f: (v: string) => string, x: T): T
// Returns array with first n elements removed
export declare const drop: <T>(n: number, xs: Iterable<T>) => T[];
// Transform object values
mapVals<V, U>(f: (v: V) => U, x: Record<string, V>): Record<string, U>
// Returns first n elements of array
export declare const take: <T>(n: number, xs: Iterable<T>) => T[];
// Merge objects (left priority)
mergeLeft<T extends Obj>(a: T, b: T): T
// Concatenates multiple arrays, filtering out null/undefined
export declare const concat: <T>(...xs: T[][]) => T[];
// Merge objects (right priority)
mergeRight<T extends Obj>(a: T, b: T): T
// Appends element to array
export declare const append: <T>(x: T, xs: T[]) => T[];
// Deep merge objects
deepMergeLeft(a: Obj, b: Obj): Obj
deepMergeRight(a: Obj, b: Obj): Obj
// Creates union of two arrays
export declare const union: <T>(a: T[], b: T[]) => T[];
// Returns elements common to both arrays
export declare const intersection: <T>(a: T[], b: T[]) => T[];
// Returns elements in first array not present in second
export declare const difference: <T>(a: T[], b: T[]) => T[];
// Removes all instances of an element from array
export declare const remove: <T>(a: T, xs: T[]) => T[];
// Returns elements from second array not present in first
export declare const without: <T>(a: T[], b: T[]) => T[];
// Toggles presence of element in array
export declare const toggle: <T>(x: T, xs: T[]) => T[];
// Generates sequence of numbers from a to b
export declare function range(a: number, b: number, step?: number): Generator<number, void, unknown>;
// Yields indexed items
export declare function enumerate<T>(items: T[]): Generator<[number, T], void, unknown>;
// Returns a function that gets property value from object
export declare const pluck: <T>(k: string, xs: Record<string, unknown>[]) => T[];
// Creates object from array of key-value pairs
export declare const fromPairs: <T>(pairs: [k?: string, v?: T, ...args: unknown[]][]) => Record<string, T>;
// Flattens array of arrays into single array
export declare const flatten: <T>(xs: T[][]) => T[];
// Splits array into two arrays based on predicate
export declare const partition: <T>(f: (x: T) => boolean, xs: T[]) => T[][];
// Returns array with duplicate elements removed
export declare const uniq: <T>(xs: T[]) => T[];
// Returns array with elements unique by key function
export declare const uniqBy: <T>(f: (x: T) => any, xs: T[]) => T[];
// Returns sorted copy of array
export declare const sort: <T>(xs: T[]) => T[];
// Returns array sorted by key function
export declare const sortBy: <T>(f: (x: T) => any, xs: T[]) => T[];
// Groups array elements by key function
export declare const groupBy: <T, K>(f: (x: T) => K, xs: T[]) => Map<K, T[]>;
// Creates map from array using key function
export declare const indexBy: <T, K>(f: (x: T) => K, xs: T[]) => Map<K, T>;
// Creates array of specified length using generator function
export declare const initArray: <T>(n: number, f: () => T) => T[];
// Splits array into chunks of specified length
export declare const chunk: <T>(chunkLength: number, xs: T[]) => T[][];
// Splits array into specified number of chunks
export declare const chunks: <T>(n: number, xs: T[]) => T[][];
// Splits array into two parts at index
export declare const splitAt: <T>(n: number, xs: T[]) => T[][];
// Inserts element into array at index
export declare const insert: <T>(n: number, x: T, xs: T[]) => T[];
// Returns random element from array
export declare const choice: <T>(xs: T[]) => T;
// Returns shuffled copy of iterable
export declare const shuffle: <T>(xs: Iterable<T>) => T[];
// Returns n random elements from array
export declare const sample: <T>(n: number, xs: T[]) => T[];
// Checks if value is iterable
export declare const isIterable: (x: any) => boolean;
// Ensures value is iterable by wrapping in array if needed
export declare const toIterable: (x: any) => any;
// Ensures value is array by wrapping if needed
export declare const ensurePlural: <T>(x: T | T[]) => T[];
```
### Number Operations
## Objects
```typescript
// Convert Maybe<number> to number
num(x: Maybe<number>): number
// Checks if value is a plain object
export declare const isPojo: (obj: any) => boolean;
// Basic arithmetic with Maybe<number>
add(x: Maybe<number>, y: Maybe<number>): number
sub(x: Maybe<number>, y: Maybe<number>): number
mul(x: Maybe<number>, y: Maybe<number>): number
div(x: Maybe<number>, y: number): number
// Creates new object with only specified keys
export declare const pick: <T extends Obj>(ks: string[], x: T) => T;
// Increment/Decrement
inc(x: Maybe<number>): number
dec(x: Maybe<number>): number
// Creates new object with specified keys removed
export declare const omit: <T extends Obj>(ks: string[], x: T) => T;
// Comparisons
lt(x: Maybe<number>, y: Maybe<number>): boolean
lte(x: Maybe<number>, y: Maybe<number>): boolean
gt(x: Maybe<number>, y: Maybe<number>): boolean
gte(x: Maybe<number>, y: Maybe<number>): boolean
// Creates new object excluding entries with specified values
export declare const omitVals: <T extends Obj>(xs: any[], x: T) => T;
// Array number operations
max(xs: Maybe<number>[]): number
min(xs: Maybe<number>[]): number
sum(xs: Maybe<number>[]): number
avg(xs: Maybe<number>[]): number
// Filters object values based on predicate
export declare const filterVals: <T extends Record<string, any>>(f: (v: any) => boolean, x: T) => T;
// Creates new object with transformed keys
export declare const mapKeys: <T extends Obj>(f: (v: string) => string, x: T) => T;
// Creates new object with transformed values
export declare const mapVals: <V, U>(f: (v: V) => U, x: Record<string, V>) => Record<string, U>;
// Merges two objects, with left object taking precedence
export declare const mergeLeft: <T extends Obj>(a: T, b: T) => T;
// Merges two objects, with right object taking precedence
export declare const mergeRight: <T extends Obj>(a: T, b: T) => T;
// Deep merge two objects, prioritizing the first argument.
export declare const deepMergeLeft: (a: Obj, b: Obj) => Obj<any>;
// Deep merge two objects, prioritizing the second argument.
export declare const deepMergeRight: (a: Obj, b: Obj) => Obj<any>;
// Switches on key in object, with default fallback
export declare const switcher: <T>(k: string, m: Record<string, T>) => T;
```
### String Operations
## Combinators
```typescript
// Truncate string with ellipsis
ellipsize(s: string, l: number, suffix = "..."): string
// Returns a function that returns the boolean negation of the given function
export declare const complement: <T extends unknown[]>(f: (...args: T) => any) => (...args: T) => boolean;
// URL operations
stripProtocol(url: string): string
displayUrl(url: string): string
displayDomain(url: string): string
// Safely executes function and handles errors
export declare const tryCatch: <T>(f: () => T, onError?: (e: Error) => void) => T | undefined;
// Bech32 encoding/decoding
hexToBech32(prefix: string, hex: string): string
bech32ToHex(b32: string): string
// Creates function that only executes once
export declare const once: (f: (...args: any) => void) => (...args: any) => void;
// Calls a function
export declare const call: <T>(f: () => T, ...args: unknown[]) => T;
// Memoizes function results based on arguments
export declare const memoize: <T>(f: (...args: any[]) => T) => (...args: any[]) => T;
// Executes a function if the value is defined
export declare const ifLet: <T>(x: T | undefined, f: (x: T) => void) => void;
```
### Collection Operations
## Randomness
```typescript
// Create union of arrays
union<T>(a: T[], b: T[]): T[]
// Generates random integer between min and max (inclusive)
export declare const randomInt: (min?: number, max?: number) => number;
// Get intersection of arrays
intersection<T>(a: T[], b: T[]): T[]
// Get difference of arrays
difference<T>(a: T[], b: T[]): T[]
// Remove element from array
remove<T>(a: T, xs: T[]): T[]
// Filter array by another array
without<T>(a: T[], b: T[]): T[]
// Toggle element in array
toggle<T>(x: T, xs: T[]): T[]
// Group array by key function
groupBy<T, K>(f: (x: T) => K, xs: T[]): Map<K, T[]>
// Create map from array
indexBy<T, K>(f: (x: T) => K, xs: T[]): Map<K, T>
// Generates random string ID
export declare const randomId: () => string;
```
### Time Constants
## Async
```typescript
const MINUTE = 60
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
const WEEK = 7 * DAY
const MONTH = 30 * DAY
const QUARTER = 90 * DAY
const YEAR = 365 * DAY
// Creates a promise that resolves after specified time
export declare const sleep: (t: number) => Promise<unknown>;
// Get current timestamp in seconds
now(): number
// Creates a microtask that yields to other tasks in the event loop
export declare const yieldThread: () => any;
// Get timestamp from ago in seconds
ago(unit: number, count = 1): number
// Creates throttled version of function
export declare const throttle: <F extends (...args: any[]) => any>(ms: number, f: F) => F | ((...thisArgs: Parameters<F>) => void);
// Convert seconds to milliseconds
ms(seconds: number): number
// Creates throttled function that returns cached value
export declare const throttleWithValue: <T>(ms: number, f: () => T) => () => T;
// Creates batching function that collects items
export declare const batch: <T>(t: number, f: (xs: T[]) => void) => (x: T) => void;
// Creates batching function that returns results
export declare const batcher: <T, U>(t: number, execute: (request: T[]) => U[] | Promise<U[]>) => (request: T) => Promise<U>;
```
### Function Utilities
## URLs
```typescript
// Create function that executes once
once(f: (...args: any) => void): (...args: any) => void
// Removes protocol (http://, https://, etc) from URL
export declare const stripProtocol: (url: string) => string;
// Memoize function results
memoize<T>(f: (...args: any[]) => T): (...args: any[]) => T
// Formats URL for display by removing protocol, www, and trailing slash
export declare const displayUrl: (url: string) => string;
// Create throttled function
throttle<F extends (...args: any[]) => any>(
ms: number,
f: F
): F
// Create batching function
batch<T>(
t: number,
f: (xs: T[]) => void
): (x: T) => void
// Extracts and formats domain from URL
export declare const displayDomain: (url: string) => string;
```
### Network Utilities
## JSON, localStorage, fetch, event emitters, etc
```typescript
// Fetch JSON with options
fetchJson(url: string, opts?: FetchOpts): Promise<any>
// Safely parses JSON string
export declare const parseJson: (json: string | undefined) => any;
// Post JSON data
postJson<T>(url: string, data: T, opts?: FetchOpts): Promise<any>
// Gets and parses JSON from localStorage
export declare const getJson: (k: string) => any;
// Upload file
uploadFile(url: string, file: File): Promise<any>
// Stringifies and stores value in localStorage
export declare const setJson: (k: string, v: any) => void;
// Options for fetch requests
type FetchOpts = {
method?: string;
headers?: Record<string, string | boolean>;
body?: string | FormData;
};
// Fetches JSON from URL with options
export declare const fetchJson: (url: string, opts?: FetchOpts) => Promise<any>;
// Posts JSON data to URL
export declare const postJson: <T>(url: string, data: T, opts?: FetchOpts) => Promise<any>;
// Uploads file to URL
export declare const uploadFile: (url: string, file: File) => Promise<any>;
// A generic type-safe event listener function that works with event emitters.
export declare const on: <EventMap extends Record<string | symbol, any[]>, E extends keyof EventMap>(target: {
on(event: E, listener: (...args: EventMap[E]) => any): any;
off(event: E, listener: (...args: EventMap[E]) => any): any;
}, eventName: E, callback: (...args: EventMap[E]) => void) => (() => void);
```
## Usage Examples
## Strings
```typescript
// Array operations
const nums = [1, 2, 2, 3, 3, 3]
uniq(nums) // => [1, 2, 3]
// Truncates string to length, breaking at word boundaries
export declare const ellipsize: (s: string, l: number, suffix?: string) => string;
// Object operations
const obj = {a: 1, b: 2, c: 3}
omit(['a', 'b'], obj) // => {c: 3}
// Number operations
add(5, undefined) // => 5
inc(undefined) // => 1
// Time operations
ago(DAY, 2) // => timestamp from 2 days ago
// URL operations
displayUrl('https://www.example.com/') // => 'example.com'
// Collection operations
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}]
indexBy(u => u.id, users) // => Map(1 => {id: 1, name: 'Alice'}, ...)
// Function utilities
const throttledFn = throttle(1000, () => console.log('throttled'))
// Generates a hash string from input string
export declare const hash: (s: string) => string;
```
## Curried utilities for working with collections
```typescript
// Returns a function that gets the nth element of an array
export declare const nth: (i: number) => <T>(xs: T[], ...args: unknown[]) => T;
// Returns a function that checks if nth element equals value
export declare const nthEq: (i: number, v: any) => (xs: any[], ...args: unknown[]) => boolean;
// Returns a function that checks if nth element does not equal value
export declare const nthNe: (i: number, v: any) => (xs: any[], ...args: unknown[]) => boolean;
// Returns a function that checks if key/value pairs of x match all pairs in spec
export declare const spec: (values: Obj | Array<any>) => (x: Obj | Array<any>, ...args: unknown[]) => boolean;
// Returns a function that checks equality with value
export declare const eq: <T>(v: T) => (x: T) => boolean;
// Returns a function that checks inequality with value
export declare const ne: <T>(v: T) => (x: T) => boolean;
// Returns a function that gets property value from object
export declare const prop: <T>(k: string) => (x: Record<string, unknown>) => T;
// Returns a function that adds/updates a property on object
export declare const assoc: <K extends string, T, U>(k: K, v: T) => (o: U) => U & Record<K, T>;
// Returns a function that removes a property on object
export declare const dissoc: <K extends string, T extends Obj>(k: K) => (o: T) => T;
```
## Sets
```typescript
// Adds value to Set at key in object
export declare const addToKey: <T>(m: Record<string, Set<T>>, k: string, v: T) => void;
// Pushes value to array at key in object
export declare const pushToKey: <T>(m: Record<string, T[]>, k: string, v: T) => void;
```
## Maps
```typescript
// Adds value to Set at key in Map
export declare const addToMapKey: <K, T>(m: Map<K, Set<T>>, k: K, v: T) => void;
// Pushes value to array at key in Map
export declare const pushToMapKey: <K, T>(m: Map<K, T[]>, k: K, v: T) => void;
```
## Bech32 <-> hex encoding
```typescript
// Converts hex string to bech32 format
export declare const hexToBech32: (prefix: string, hex: string) => `${Lowercase<string>}1${string}`;
// Converts bech32 string to hex format
export declare const bech32ToHex: (b32: string) => string;
```
-117
View File
@@ -1,117 +0,0 @@
# Worker
The Worker class provides a robust queue processing system with batched operations, throttling, and message routing capabilities. It's designed to handle asynchronous operations efficiently while maintaining control over processing rates and resource usage.
## Overview
```typescript
class Worker<T> {
constructor(readonly opts: WorkerOpts<T> = {})
}
```
The Worker class accepts messages of type `T` and processes them according to configured options and handlers.
## Configuration
```typescript
type WorkerOpts<T> = {
// Function to determine routing key for messages
getKey?: (x: T) => any
// Function to check if message should be deferred
shouldDefer?: (x: T) => boolean
// Maximum messages to process in one batch
chunkSize?: number // default: 50
// Milliseconds between processing batches
delay?: number // default: 50
}
```
## Basic Usage
```typescript
// Create worker for processing messages
const worker = new Worker<Message>({
chunkSize: 10,
delay: 100,
getKey: msg => msg.type
})
// Add message handlers
worker.addHandler('email', async (msg) => {
await sendEmail(msg)
})
worker.addHandler('notification', async (msg) => {
await sendNotification(msg)
})
// Add messages to queue
worker.push({
type: 'email',
content: 'Hello'
})
```
## Features
### Message Routing
Messages can be routed to specific handlers based on a key:
```typescript
const worker = new Worker<Task>({
getKey: task => task.priority
})
// Handle high priority tasks
worker.addHandler('high', async (task) => {
await processUrgent(task)
})
// Handle normal priority tasks
worker.addHandler('normal', async (task) => {
await processNormal(task)
})
```
### Global Handlers
Handle all messages regardless of routing key:
```typescript
worker.addGlobalHandler(async (message) => {
console.log('Processing:', message)
})
```
### Message Deferral
Defer processing of messages that aren't ready:
```typescript
const worker = new Worker<Task>({
shouldDefer: (task) => !task.isReady(),
delay: 1000
})
worker.push(task) // Will retry until task.isReady()
```
### Flow Control
Control message processing:
```typescript
// Pause processing
worker.pause()
// Resume processing
worker.resume()
// Clear queue
worker.clear()
```
+62
View File
@@ -1,5 +1,7 @@
# @welshman/net
[![version](https://badgen.net/npm/v/@welshman/net)](https://npmjs.com/package/@welshman/net)
Core networking layer for nostr applications, handling relay connections, message management, and event delivery.
## What's Included
@@ -12,6 +14,66 @@ Core networking layer for nostr applications, handling relay connections, messag
- **Targets** - Flexible message routing strategies
- **Event Tracking** - Monitor which relays have seen events
## Quick Example
```typescript
import {ctx, setContext} from '@welshman/lib'
import {type TrustedEvent, createEvent, NOTE} from '@welshman/util'
import {subscribe, publish, getDefaultNetContext} from '@welshman/net'
// Sets up customizable event valdation, handlers, etc
setContext(getDefaultNetContext())
// Send a subscription
const sub = subscribe({
relays: ['wss://relay.example.com/'],
filters: [{kinds: [1], limit: 1}],
closeOnEose: true,
timeout: 10000,
})
sub.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
console.log(url, event)
sub.close()
})
// Publish an event
const pub = publish({
relays: ['wss://relay.example.com/'],
event: createEvent(NOTE, {content: 'hi'}),
})
pub.emitter.on('*', (status: PublishStatus, url: string) => {
console.log(status, url)
})
// The Tracker class can tell you which relays an event was read from or published to
console.log(ctx.net.tracker.getRelays(event.id))
```
The main reason this module exists is to support different backends via Executor and different `target` classes. For example, to add a local relay that automatically gets used:
```typescript
import {setContext} from '@welshman/lib'
import {LOCAL_RELAY_URL, Relay, Repository} from '@welshman/util'
import {getDefaultNetContext, Multi, Local, Relays, Executor} from '@welshman/net'
const repository = new Repository()
const relay = new Relay(repository)
setContext(getDefaultNetContext({
getExecutor: (relays: string[]) => {
return new Executor(
new Multi([
new Local(relay),
new Relays(remoteUrls.map(url => ctx.net.pool.get(url))),
])
)
},
}))
```
## Installation
```bash
+21
View File
@@ -0,0 +1,21 @@
# @welshman/relay
[![version](https://badgen.net/npm/v/@welshman/relay)](https://npmjs.com/package/@welshman/relay)
Core networking layer for nostr applications, handling relay connections, message management, and event delivery.
## What's Included
- **Event Store** - A Repository class which stores events in memory
- **Relay Adapter** - A LocalRelay class which adapts nostr messages to the repository
## Quick Example
```typescript
```
## Installation
```bash
npm install @welshman/relay
```
+11 -3
View File
@@ -1,8 +1,8 @@
# @welshman/signer
A comprehensive Nostr signing implementation that supports multiple authentication methods and encryption standards.
It provides a unified interface for working with different signing mechanisms while maintaining compatibility with various Nostr Implementation Possibilities (NIPs).
[![version](https://badgen.net/npm/v/@welshman/signer)](https://npmjs.com/package/@welshman/signer)
A Nostr signer implementation that supports multiple authentication methods and encryption standards.
## What's Included
@@ -13,11 +13,19 @@ It provides a unified interface for working with different signing mechanisms wh
- **NIP-55 Signer** - Native app integration via Capacitor
- **NIP-59 Utils** - Gift Wrap protocol for secure event encryption
## Quick Example
```typescript
import { makeEvent } from '@welshman/util'
import { ISigner, Nip01Signer, makeSecret } from '@welshman/signer'
const signer: ISigner = new Nip01Signer(makeSecret())
signer.sign(makeEvent(1)).then(signedEvent => console.log(signedEvent))
```
## Installation
```bash
npm install @welshman/signer
```
+17 -1
View File
@@ -1,6 +1,7 @@
# @welshman/store
A utility package designed specifically for Svelte applications, providing enhanced store functionality and utilities for managing state. While it's primarily built for use with Svelte's store system, the concepts could be valuable for developers familiar with reactive programming patterns like RxJS.
[![version](https://badgen.net/npm/v/@welshman/store)](https://npmjs.com/package/@welshman/store)
A utility package providing welshman-specific svelte store functionality and utilities for managing state. While it's primarily built for use with Svelte's store system, the concepts could be valuable for developers familiar with reactive programming patterns like RxJS.
## What's Included
@@ -10,6 +11,21 @@ A utility package designed specifically for Svelte applications, providing enhan
- **Persistence Layer** - Automatic localStorage synchronization
- **Performance Optimizations** - Throttled updates and efficient subscription management
## Quick Example
```typescript
import {Repository, NAMED_PEOPLE, TrustedEvent, PublishedList, readList} from '@welshman/util'
import {deriveEventsMapped} from '@welshman/store'
const repository = new Repository()
// Create a store that performantly maps matching events in the repository to List objects
const lists = deriveEventsMapped<PublishedList>(repository, {
filters: [{kinds: [NAMED_PEOPLE]}],
eventToItem: (event: TrustedEvent) => (event.tags.length > 1 ? readList(event) : null),
itemToEvent: (list: PublishedList) => list.event,
})
```
## Installation
-6
View File
@@ -12,9 +12,3 @@ Need just a content parser? Grab @welshman/content. Building a complex client? S
Each module is battle-tested in production, designed to work together but never dependent on each other.
Build your next Nostr client with the same tools that power today's leading Nostr applications.
<!-- ## Installation
```sh
npm install @welshman
``` -->
-73
View File
@@ -1,73 +0,0 @@
# @welshman/app [![version](https://badgen.net/npm/v/@welshman/app)](https://npmjs.com/package/@welshman/app)
Utilities for dealing with svelte stores when using welshman.
```typescript
import {ctx, setContext} from '@welshman/lib'
import {getNip07} from '@welshman/signer'
import {throttled} from '@welshman/store'
import {createEvent, NOTE} from '@welshman/util'
import {
getDefaultNetContext,
getDefaultAppContext,
signer,
pubkey,
publishThunk,
load,
initStorage,
storageAdapters,
freshness,
plaintext,
repository,
tracker,
} from '@welshman/app'
// Set up app config
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext(),
})
// Log in via NIP 07
addSession({method: 'nip07', pubkey: await getNip07().getPubkey()})
// Signer is ready to go
const event = signer.get().encrypt(/* ... */)
// This will fetch the user's profile automatically, and return an observable that updates
// automatically. Several different stores exist that are ready to go, including handles,
// zappers, relaySelections, relays, follows, mutes.
const profile = deriveProfile(pubkey.get())
// A global router helps make intelligent relay selections
const router = ctx.app.router
// 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
const thunk = publishThunk({
relays: router.FromUser().getUrls(),
event: createEvent(NOTE, {content: "hi"}),
delay: 3000,
})
// Thunks can be aborted until after `delay`, allowing for soft-undo
thunk.controller.abort()
// Subscriptions automatically infer relays using `router` if not provided. If the request can be cached,
// results from the local repository are returned immediately. `subscribe` and `load` are both available
const events = await load({filters: [{kinds: [NOTE]}])
// Some commands are included
const thunk = follow('97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322')
// Stores can be easily synchronized with indexeddb. Freshness keeps track of how stale the caches are,
// plaintext maps encrypted events to their decrypted content, repository and tracker hold events and
// event/relay mappings, respectively.
const ready = initStorage("my-db", 1, {
relays: {keyPath: "url", store: throttled(3000, relays)},
handles: {keyPath: "nip05", store: throttled(3000, handles)},
freshness: storageAdapters.fromObjectStore(freshness, {throttle: 3000}),
plaintext: storageAdapters.fromObjectStore(plaintext, {throttle: 3000}),
events: storageAdapters.fromRepositoryAndTracker(repository, tracker, {throttle: 3000}),
})
```
+103
View File
@@ -0,0 +1,103 @@
import {get, derived} from 'svelte/store'
import {batch, fromPairs} from '@welshman/lib'
import {PROFILE, FOLLOWS, MUTES, RELAYS, INBOX_RELAYS, getPubkeyTagValues, getListTags} from '@welshman/util'
import {throttled, withGetter} from '@welshman/store'
import {RepositoryUpdate} from '@welshman/relay'
import {getAll, bulkPut, bulkDelete} from './storage.js'
import {relays} from './relays.js'
import {handles, onHandle} from './handles.js'
import {zappers, onZapper} from './zappers.js'
import {plaintext} from './plaintext.js'
import {freshness} from './freshness.js'
import {repository} from './core.js'
import {sessions} from './session.js'
import {userFollows} from './user.js'
export const defaultStorageAdapters = {
relays: {
keyPath: "url",
init: async () => relays.set(await getAll("relays")),
sync: () => throttled(3000, relays).subscribe($relays => bulkPut("relays", $relays)),
},
handles: {
keyPath: "nip05",
init: async () => handles.set(await getAll("handles")),
sync: () => onHandle(batch(300, $handles => bulkPut("handles", $handles))),
},
zappers: {
keyPath: "lnurl",
init: async () => zappers.set(await getAll("zappers")),
sync: () => onZapper(batch(300, $zappers => bulkPut("zappers", $zappers))),
},
freshness: {
keyPath: "key",
init: async () => {
const items = await getAll("freshness")
freshness.set(fromPairs(items.map(item => [item.key, item.value])))
},
sync: () => {
const interval = setInterval(() => {
bulkPut(
"freshness",
Object.entries(freshness.get()).map(([key, value]) => ({key, value})),
)
}, 10_000)
return () => clearInterval(interval)
},
},
plaintext: {
keyPath: "key",
init: async () => {
const items = await getAll("plaintext")
plaintext.set(fromPairs(items.map(item => [item.key, item.value])))
},
sync: () => {
const interval = setInterval(() => {
bulkPut(
"plaintext",
Object.entries(plaintext.get()).map(([key, value]) => ({key, value})),
)
}, 10_000)
return () => clearInterval(interval)
},
},
events: {
keyPath: "id",
init: async () => repository.load(await getAll("events")),
sync: () => {
const userFollowPubkeys = withGetter(
derived(userFollows, l => new Set(getPubkeyTagValues(getListTags(l))))
)
const onUpdate = async ({added, removed}: RepositoryUpdate) => {
const sessionKeys = new Set(Object.keys(sessions.get()))
const metaKinds = [PROFILE, FOLLOWS, MUTES, RELAYS, INBOX_RELAYS]
if (removed.size > 0) {
await bulkDelete("events", Array.from(removed))
}
if (added.length > 0) {
await bulkPut(
"events",
added.filter(e => {
if (sessionKeys.has(e.pubkey)) return true
if (e.tags.some(t => sessionKeys.has(t[1]))) return true
if (metaKinds.includes(e.kind) && userFollowPubkeys.get()?.has(e.pubkey)) return true
return false
}),
)
}
}
repository.on("update", onUpdate)
return () => repository.off("update", onUpdate)
},
},
}
+3 -3
View File
@@ -1,5 +1,5 @@
import {readable, derived, type Readable, type Subscriber} from "svelte/store"
import {indexBy, remove, type Maybe, now} from "@welshman/lib"
import {indexBy, remove, now} from "@welshman/lib"
import {withGetter} from "@welshman/store"
import {getFreshness, setFreshnessThrottled} from "./freshness.js"
@@ -15,7 +15,7 @@ export const collection = <T>({
load?: (key: string, relays: string[]) => Promise<any>
}) => {
const indexStore = withGetter(derived(store, $items => indexBy(getKey, $items)))
const pending = new Map<string, Promise<Maybe<T>>>()
const pending = new Map<string, Promise<T | void>>()
const loadAttempts = new Map<string, number>()
let subscribers: Subscriber<T>[] = []
@@ -76,7 +76,7 @@ export const collection = <T>({
return fresh
}
const deriveItem = (key: Maybe<string>, relays: string[] = []) => {
const deriveItem = (key: string | undefined, relays: string[] = []) => {
if (!key) {
return readable(undefined)
}
+1
View File
@@ -1,3 +1,4 @@
export * from "./adapters.js"
export * from "./context.js"
export * from "./core.js"
export * from "./collection.js"
-89
View File
@@ -1,89 +0,0 @@
# @welshman/dvm [![version](https://badgen.net/npm/v/@welshman/dvm)](https://npmjs.com/package/@welshman/dvm)
Utilities for building nostr DVMs.
# Request example
```javascript
import type {Publish, Subscription} from '@welshman/net'
import {makeDvmRequest, DVMEvent} from '@welshman/dvm'
const req = makeDvmRequest({
// Create and sign a dvm request event, including any desired tags
event: createAndSign({kind: 5300}),
// Publish and subscribe to these relays
relays: ['wss://relay.damus.io', 'wss://dvms.f7z.io'],
// Timeout defaults to 30 seconds
timeout: 30_000,
// Auto close on first result (defaults to true)
autoClose: true,
// Listen for and emit `progress` events
reportProgress: true,
})
// Listen for progress, result, etc
req.emitter.on(DVMEvent.Progress, (url, event) => console.log(event))
req.emitter.on(DVMEvent.Result, (url, event) => console.log(event))
```
# Handler example
```javascript
import {bytesToHex} from '@noble/hashes/utils'
import {generateSecretKey} from 'nostr-tools'
import {createEvent} from '@welshman/util'
import {subscribe} from '@welshman/net'
import {DVM} from '@welshman/dvm'
// Your DVM's private key. Store this somewhere safe
// const hexPrivateKey = bytesToHex(generateSecretKey())
const hexPrivateKey = '9cd387a3aa0c1abc2ef517c8402f29c069b4174e02a426491aec7566501bee67'
// Tags that we'll return as content discovery suggestions
const tags = []
// Populate the tags with music by Ainsley Costello
const sub = subscribe({
timeout: 30_000,
relays: ["wss://relay.wavlake.com"],
filters: [{
kinds: [31337],
'#p': ['8806372af51515bf4aef807291b96487ea1826c966a5596bca86697b5d8b23bc'],
}],
})
// Push event ids to our suggestions
sub.on('event', (url, e) => tags.push(["e", e.id, url]))
const dvm = new DVM({
// The private key used to sign events
sk: hexPrivateKey,
// Relays that the DVM will listen on
relays: ['wss://relay.damus.io', 'wss://dvms.f7z.io'],
// Only listen to requests tagging our dvm
requireMention: true,
// Expire results after 1 hour (the default)
expireAfter: 60 * 60,
// Handlers for various kinds
handlers: {
5300: dvm => ({
handleEvent: async function* (event) {
// DVM responses are stringified into the content
const content = JSON.stringify(tags)
// Yield our response. Kind 7000 can be used for partial results too
yield createEvent(event.kind + 1000, {content})
},
}),
}
})
// Enable logging
dvm.logEvents = true
// When you're ready
dvm.start()
// When you're done
dvm.stop()
```
-3
View File
@@ -1,3 +0,0 @@
# @welshman/editor [![version](https://badgen.net/npm/v/@welshman/editor)](https://npmjs.com/package/@welshman/editor)
A batteries-included nostr-editor.
-34
View File
@@ -1,34 +0,0 @@
# @welshman/feeds [![version](https://badgen.net/npm/v/@welshman/feeds)](https://npmjs.com/package/@welshman/feeds)
A custom feed compiler and loader for nostr. Read the spec on [wikifreedia](https://wikifreedia.xyz/cip-01/97c70a44366a6535c1).
# Example
```javascript
// Define a feed using set operations
const feed = intersectionFeed(
unionFeed(
dvmFeed({
kind: 5300,
pubkey: '19b78ccfa7c5e31e6bacbb3f2a1703f64b62017702e584440bf29a7e16263e8c',
}),
listFeed("10003:19ba654f26afd4930fd3d51baf4e26f1413b7aeec7190cd6c0cdf4d2f14cec6b:"),
)
wotFeed({min: 0.1}),
scopeFeed("global"),
)
// Create a controller, providing required context via FeedOptions
const controller = new FeedController({
feed,
request,
requestDVM,
getPubkeysForScope,
getPubkeysForWOTRange,
onEvent: event => console.log("Event", event),
onExhausted: () => console.log("Exhausted"),
})
// Load notes using the feed
const events = await controller.load(10)
```
-13
View File
@@ -1,13 +0,0 @@
# @welshman/lib [![version](https://badgen.net/npm/v/@welshman/lib)](https://npmjs.com/package/@welshman/lib)
Some general-purpose utilities for use in @welshman apps.
Includes:
- LRU cache implementation
- TaskQueue for throttling work to avoid locking up the UI
- URL normalization (taken from normalize-url)
- A global `ctx` variable which can be used for global configuration
- CustomPromise, which provides an error type, and `defer` utility
- Ramda-like utilities, but without auto-currying
- Utils for throttling, working with nil, json, fetch, deep equals, etc.
-7
View File
@@ -11,13 +11,6 @@ describe("Tools", () => {
vi.useRealTimers()
})
describe("Basic Utils", () => {
it("should check for nil values", () => {
expect(T.isNil(null)).toBe(true)
expect(T.isNil(undefined)).toBe(true)
expect(T.isNil(0)).toBe(false)
expect(T.isNil("")).toBe(false)
})
it("should handle ifLet", () => {
const fn = vi.fn()
T.ifLet(undefined, fn)
+711 -651
View File
File diff suppressed because it is too large Load Diff
-61
View File
@@ -1,61 +0,0 @@
# @welshman/net [![version](https://badgen.net/npm/v/@welshman/net)](https://npmjs.com/package/@welshman/net)
Utilities having to do with connection management and nostr messages.
```typescript
import {ctx, setContext} from '@welshman/lib'
import {type TrustedEvent, createEvent, NOTE} from '@welshman/util'
import {subscribe, publish, getDefaultNetContext} from '@welshman/net'
// Sets up customizable event valdation, handlers, etc
setContext(getDefaultNetContext())
// Send a subscription
const sub = subscribe({
relays: ['wss://relay.example.com/'],
filters: [{kinds: [1], limit: 1}],
closeOnEose: true,
timeout: 10000,
})
sub.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
console.log(url, event)
sub.close()
})
// Publish an event
const pub = publish({
relays: ['wss://relay.example.com/'],
event: createEvent(NOTE, {content: 'hi'}),
})
pub.emitter.on('*', (status: PublishStatus, url: string) => {
console.log(status, url)
})
// The Tracker class can tell you which relays an event was read from or published to
console.log(ctx.net.tracker.getRelays(event.id))
```
The main reason this module exists is to support different backends via Executor and different `target` classes. For example, to add a local relay that automatically gets used:
```typescript
import {setContext} from '@welshman/lib'
import {LOCAL_RELAY_URL, Relay, Repository} from '@welshman/util'
import {getDefaultNetContext, Multi, Local, Relays, Executor} from '@welshman/net'
const repository = new Repository()
const relay = new Relay(repository)
setContext(getDefaultNetContext({
getExecutor: (relays: string[]) => {
return new Executor(
new Multi([
new Local(relay),
new Relays(remoteUrls.map(url => ctx.net.pool.get(url))),
])
)
},
}))
```
+2
View File
@@ -166,3 +166,5 @@ export class MultiPublish extends EventEmitter {
this.removeAllListeners()
}
}
export const publish = (options: MultiPublishOptions) => new MultiPublish(options)
-61
View File
@@ -1,61 +0,0 @@
# @welshman/net [![version](https://badgen.net/npm/v/@welshman/net)](https://npmjs.com/package/@welshman/net)
Utilities having to do with connection management and nostr messages.
```typescript
import {ctx, setContext} from '@welshman/lib'
import {type TrustedEvent, createEvent, NOTE} from '@welshman/util'
import {subscribe, publish, getDefaultNetContext} from '@welshman/net'
// Sets up customizable event valdation, handlers, etc
setContext(getDefaultNetContext())
// Send a subscription
const sub = subscribe({
relays: ['wss://relay.example.com/'],
filters: [{kinds: [1], limit: 1}],
closeOnEose: true,
timeout: 10000,
})
sub.on(SubscriptionEvent.Event, (url: string, event: TrustedEvent) => {
console.log(url, event)
sub.close()
})
// Publish an event
const pub = publish({
relays: ['wss://relay.example.com/'],
event: createEvent(NOTE, {content: 'hi'}),
})
pub.emitter.on('*', (status: PublishStatus, url: string) => {
console.log(status, url)
})
// The Tracker class can tell you which relays an event was read from or published to
console.log(ctx.net.tracker.getRelays(event.id))
```
The main reason this module exists is to support different backends via Executor and different `target` classes. For example, to add a local relay that automatically gets used:
```typescript
import {setContext} from '@welshman/lib'
import {LOCAL_RELAY_URL, Relay, Repository} from '@welshman/util'
import {getDefaultNetContext, Multi, Local, Relays, Executor} from '@welshman/net'
const repository = new Repository()
const relay = new Relay(repository)
setContext(getDefaultNetContext({
getExecutor: (relays: string[]) => {
return new Executor(
new Multi([
new Local(relay),
new Relays(remoteUrls.map(url => ctx.net.pool.get(url))),
])
)
},
}))
```
+5
View File
@@ -29,6 +29,11 @@ const getDay = (ts: number) => Math.floor(ts / DAY)
export let repositorySingleton: Repository<TrustedEvent>
export type RepositoryUpdate = {
added: TrustedEvent[]
removed: Set<string>
}
export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
eventsById = new Map<string, E>()
eventsByWrap = new Map<string, E>()
-93
View File
@@ -1,93 +0,0 @@
# @welshman/signer [![version](https://badgen.net/npm/v/@welshman/signer)](https://npmjs.com/package/@welshman/signer)
Implementations of signer utilities and classes.
## Nips supported
- NIP 01 (private key login)
- NIP 07
- NIP 46
- NIP 55
- NIP 59 (gift wrapping, works with any signer that supports encryption)
## Examples
### NIP 01
```typescript
import {makeSecret, Nip01Signer} from '@welshman/signer'
const signer = Nip01Signer.fromSecret(makeSecret())
```
### NIP 07
```typescript
import {getNip07, Nip07Signer} from '@welshman/signer'
if (getNip07()) {
const signer = new Nip07Signer()
}
```
### NIP 55
```typescript
import {getNip07, Nip07Signer} from '@welshman/signer'
if (getNip07()) {
const signer = new Nip07Signer()
}
```
### NIP 46
```typescript
import {createEvent, NOTE} from '@welshman/util'
import {makeSecret, Nip46Broker, Nip46Signer} from '@welshman/signer'
const clientSecret = makeSecret()
const relays = ['wss://relay.signer.example/']
const broker = Nip46Broker.get({relays, clientSecret})
const signer = new Nip46Signer(broker)
const ncUrl = broker.makeNostrconnectUrl({name: "My app"})
const abortController = new AbortController()
let response
try {
response = await broker.waitForNostrconnect(url, abortController)
} catch (e: any) {
if (e?.error) {
showWarning(`Received error from signer: ${e.error}`)
} else if (e) {
console.error(e)
}
}
if (response) {
// Now we know the bunker's pubkey and can do stuff with the signer
const signerPubkey = response.event.pubkey
// Next time we want to use our signer, we can instantiate it like so:
const newBroker = Nip46Broker.get({relays, clientSecret, signerPubkey})
const newSigner = new Nip46Signer(newBroker)
}
```
### Using signers
```typescript
import {createEvent, NOTE, DIRECT_MESSAGE} from '@welshman/util'
const signer = // Create your signer...
const nip59 = Nip59.fromSigner(signer)
// Sign an event
const event = await signer.sign(createEvent(NOTE, {content: "hi"}))
// Wrap a NIP 17 DM
const rumor = await nip59.wrap(recipientPubkey, createEvent(DIRECT_MESSAGE, {content: "hi"}))
// Note that it returns a rumor; be sure to publish the `wrap`
const wrap = rumor.wrap
```
+1 -2
View File
@@ -10,7 +10,6 @@ import {
partition,
first,
} from "@welshman/lib"
import {Maybe} from "@welshman/lib"
import {Repository} from "@welshman/relay"
import {matchFilters, getIdAndAddress, getIdFilters, Filter, TrustedEvent} from "@welshman/util"
@@ -142,7 +141,7 @@ export const adapter = <Source, Target>({
export type DeriveEventsMappedOptions<T> = {
filters: Filter[]
eventToItem: (event: TrustedEvent) => Maybe<T | T[] | Promise<T | T[]>>
eventToItem: (event: TrustedEvent) => T | T[] | Promise<T | T[]> | undefined
itemToEvent: (item: T) => TrustedEvent
throttle?: number
includeDeleted?: boolean
-15
View File
@@ -1,15 +0,0 @@
# @welshman/util [![version](https://badgen.net/npm/v/@welshman/util)](https://npmjs.com/package/@welshman/util)
Some nostr-specific utilities. For the most part, these will not have side effects or manage state. Includes:
- Event kind constants
- A nostr address class
- Utilities for working with nostr filters and tags
- Helpers for working with zap events and lightning invoices
- A `Encryptable` for ensuring payloads get encrypted
- An implementation of an in-memory relay, backed by an events repository
- Utilities for building events, validating signatures, and checking event type (replaceable, etc.)
- Types and utilities for NIP 89 handlers
- Types and utilities for NIP 51 lists
- Types and utilities for NIP 01 profile metadata
- Types and utilities for NIP 11 relay profiles