Finish pass on docs

This commit is contained in:
Jon Staab
2025-06-10 15:48:00 -07:00
parent 1a81768e8e
commit ee6fa8b956
19 changed files with 545 additions and 435 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
node_modules
#docs
docs
docs/reference
docs/.vitepress/cache
build
+29 -16
View File
@@ -38,18 +38,22 @@ export default defineConfig({
text: "@welshman/util",
link: "/util/",
items: [
{text: "Address", link: "/util/address"},
{text: "Kinds", link: "/util/kinds"},
{text: "Encryptable", link: "/util/encryptable"},
{text: "Address", link: "/util/address"},
{text: "Links", link: "/util/links"},
{text: "Events", link: "/util/events"},
{text: "Filters", link: "/util/filters"},
{text: "Handlers", link: "/util/handlers"},
{text: "Links", link: "/util/links"},
{text: "Profile", link: "/util/profile"},
{text: "Relay", link: "/util/relay"},
{text: "Repository", link: "/util/repository"},
{text: "Tags", link: "/util/tags"},
{text: "Encryptable", link: "/util/encryptable"},
{text: "Relays", link: "/util/relay"},
{text: "Profiles", link: "/util/profile"},
{text: "Handlers", link: "/util/handlers"},
{text: "Lists", link: "/util/list"},
{text: "Zaps", link: "/util/zaps"},
{text: "Relay Auth", link: "/util/nip42"},
{text: "HTTP Auth", link: "/util/nip98"},
{text: "Blossom", link: "/util/blossom"},
{text: "Relay Management", link: "/util/nip86"},
],
},
{
@@ -74,18 +78,22 @@ export default defineConfig({
link: "/signer/",
items: [
{text: "ISigner", link: "/signer/isigner"},
{text: "NIP-01", link: "/signer/nip-01"},
{text: "NIP-07", link: "/signer/nip-07"},
{text: "NIP-46", link: "/signer/nip-46"},
{text: "NIP-55", link: "/signer/nip-55"},
{text: "NIP-59", link: "/signer/nip-59"},
{text: "NIP 01", link: "/signer/nip-01"},
{text: "NIP 07", link: "/signer/nip-07"},
{text: "NIP 46", link: "/signer/nip-46"},
{text: "NIP 55", link: "/signer/nip-55"},
{text: "NIP 59", link: "/signer/nip-59"},
],
},
{
text: "@welshman/relay",
link: "/relay/",
items: [
],
items: [],
},
{
text: "@welshman/router",
link: "/router/",
items: [],
},
{
text: "@welshman/content",
@@ -114,8 +122,12 @@ export default defineConfig({
text: "@welshman/store",
link: "/store/",
items: [
{text: "Basic utilities", link: "/store/basic"},
{text: "Event stores", link: "/store/events"},
{text: "Throttled", link: "/store/throttle"},
{text: "Synced", link: "/store/synced"},
{text: "Getter", link: "/store/getter"},
{text: "Custom", link: "/store/custom"},
{text: "Repository", link: "/store/repository"},
{text: "Collections", link: "/store/collection"},
],
},
{
@@ -127,6 +139,7 @@ export default defineConfig({
{text: "Task Queue", link: "/lib/task-queue"},
{text: "Normalize URL", link: "/lib/normalize-url"},
{text: "Deferred", link: "/lib/deferred"},
{text: "Emitter", link: "/lib/emitter"},
],
},
],
+53 -1
View File
@@ -2,7 +2,7 @@
[![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.
A few utilites for storing nostr events in memory.
## What's Included
@@ -12,6 +12,58 @@ Core networking layer for nostr applications, handling relay connections, messag
## Quick Example
```typescript
import {Repository, LocalRelay} from "@welshman/relay"
// Create an in-memory event repository
const repository = Repository.get()
// Publish events directly to the repository
const textNote = {
id: "event123",
pubkey: "author-pubkey",
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: "Hello, world!",
sig: "signature"
}
repository.publish(textNote)
// Query events using filters
const recentNotes = repository.query([{kinds: [1], limit: 10}])
console.log(`Found ${recentNotes.length} text notes`)
// Listen for repository updates
repository.on("update", ({added, removed}) => {
console.log(`Added ${added.length} events, removed ${removed.size} events`)
})
// Create a local relay that adapts Nostr messages to the repository
const relay = new LocalRelay(repository)
// Listen for relay messages
relay.on("EVENT", (subId, event) => {
console.log(`Received event ${event.id} for subscription ${subId}`)
})
relay.on("OK", (eventId, success, message) => {
console.log(`Event ${eventId} ${success ? "accepted" : "rejected"}: ${message}`)
})
// Use relay protocol to publish and subscribe
relay.send("EVENT", {
id: "event456",
pubkey: "another-author",
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [["t", "welshman"]],
content: "Using LocalRelay!",
sig: "signature"
})
// Subscribe to events with hashtag
relay.send("REQ", "tagged", {kinds: [1], "#t": ["welshman"]})
```
## Installation
+77 -12
View File
@@ -8,37 +8,47 @@ Utilities for selecting nostr relays.
- **Router** - A configurable router class usable as a singleton which provides common relay selection scenarios.
- **RouterScenario** - A scenario class which scores relays based on policy.
- **getFilterSelections** - A high-level utility for inferring relay selections from fitlers.
- **getFilterSelections** - A high-level utility for inferring relay selections from filters.
- **Fallback Policies** - Functions to determine how many fallback relays to add.
## Quick Example
```typescript
import {routerContext, addMaximalFallbacks, Router} from '@welshman/router'
import {Router, addMaximalFallbacks, getFilterSelections} from '@welshman/router'
// Configure the global router instance based on RouterOptions
// Configure the global router instance
Router.configure({
defaultRelays: ['wss://relay.example.com/'],
getDefaultRelays: () => ['wss://relay.example.com/'],
getPubkeyRelays: (pubkey, mode) => ['wss://myrelay.example.com/'],
getIndexerRelays: () => ['wss://indexer.example.com/'],
getUserPubkey: () => 'user-pubkey',
getRelayQuality: (url) => 0.8,
getLimit: () => 5
})
// Get the singleton and use it to select some relays
const router = Router.get()
// Get a hint based on pubkey
router.FromPubkeys(pubkeys).getUrl()
// Get relays for reading events from specific pubkeys
const readRelays = router.FromPubkeys(['pubkey1', 'pubkey2']).getUrls()
// Send an event to the author's outbox and mentions' inboxes
router.PublishEvent(event).getUrls()
// Get relays for publishing an event (author's outbox + mentions' inboxes)
const publishRelays = router.PublishEvent(event).getUrls()
// Try as hard as we can to find a quoted note
router
.FromPubkeys(event, quotedEventId, hints)
// Try hard to find a quoted note with maximal fallbacks
const searchRelays = router
.Quote(event, quotedEventId, hints)
.allowLocal(true)
.allowOnion(true)
.allowInsecure(true)
.policy(addMaximalFallbacks)
.limit(10)
.getUrls()
// Automatically select relays based on filters
const relaysAndFilters = getFilterSelections([
{kinds: [1], authors: ['pubkey1', 'pubkey2']},
{kinds: [0], search: 'bitcoin'}
])
```
## Installation
@@ -46,3 +56,58 @@ router
```bash
npm install @welshman/router
```
## Core Concepts
### Router
The main class for relay selection. Configure it once with your relay discovery functions, then use scenario methods to select relays for different purposes.
**Configuration Options:**
- `getUserPubkey()` - Returns the current user's pubkey
- `getPubkeyRelays(pubkey, mode)` - Returns relays for a pubkey ("read", "write", or "inbox")
- `getDefaultRelays()` - Returns fallback relays
- `getIndexerRelays()` - Returns relays that index profiles and relay lists
- `getSearchRelays()` - Returns relays that support NIP-50 search
- `getRelayQuality(url)` - Returns quality score (0-1) for a relay
- `getLimit()` - Returns maximum number of relays to select
**Scenario Methods:**
- `FromRelays(relays)` - Use specific relays
- `ForUser()` / `FromUser()` / `UserInbox()` - User's read/write/inbox relays
- `ForPubkey(pubkey)` / `FromPubkey(pubkey)` / `PubkeyInbox(pubkey)` - Pubkey's relays
- `ForPubkeys(pubkeys)` / `FromPubkeys(pubkeys)` - Multiple pubkeys' relays
- `Event(event)` - Relays for an event's author
- `PublishEvent(event)` - Relays for publishing (author + mentions)
- `Quote(event, id, hints)` - Relays for finding a quoted event
- `Search()` / `Index()` / `Default()` - Special relay types
### RouterScenario
Represents a relay selection with scoring and filtering options.
**Methods:**
- `getUrls()` - Returns selected relay URLs
- `getUrl()` - Returns first selected relay URL
- `limit(n)` - Limit number of relays
- `weight(scale)` - Scale selection weight
- `policy(fallbackPolicy)` - Set fallback policy
- `allowLocal(bool)` / `allowOnion(bool)` / `allowInsecure(bool)` - Filter relay types
### Fallback Policies
Functions that determine how many fallback relays to add:
- `addNoFallbacks` - Never add fallbacks
- `addMinimalFallbacks` - Add 1 fallback if no relays found
- `addMaximalFallbacks` - Fill up to the limit with fallbacks
### Filter Selection
`getFilterSelections(filters)` automatically chooses appropriate relays based on filter content:
- Search filters → search relays
- Wrap events → user's inbox
- Profile/relay kinds → indexer relays
- Author filters → authors' relays
- Everything else → user's relays (low weight)
Returns `RelaysAndFilters[]` with optimized relay-filter combinations.
+1 -17
View File
@@ -8,7 +8,7 @@ The implementation consists of two main classes:
- `Nip46Broker`: Handles the communication with the remote signer
- `Nip46Signer`: Implements the `ISigner` interface using the broker
## Getting Started
## Example
```typescript
import {
@@ -124,22 +124,6 @@ Nip46Broker.parseBunkerUrl(url: string): {
}
```
### Remote Operations
```typescript
// Basic operations
broker.ping(): Promise<string>
broker.getPublicKey(): Promise<string>
broker.connect(connectSecret?: string, perms?: string): Promise<string>
// Signing and encryption
broker.signEvent(event: StampedEvent): Promise<SignedEvent>
broker.nip04Encrypt(pk: string, message: string): Promise<string>
broker.nip04Decrypt(pk: string, message: string): Promise<string>
broker.nip44Encrypt(pk: string, message: string): Promise<string>
broker.nip44Decrypt(pk: string, message: string): Promise<string>
```
## Nip46Signer Usage
```typescript
-60
View File
@@ -1,60 +0,0 @@
# Basic Utilities
## synced
Creates a writable store that automatically synchronizes its value with localStorage.
```typescript
const myStore = synced('storage-key', 'default value');
```
## getter
Creates a function that returns the current value of a store without subscribing to it.
```typescript
const myStore = writable('value');
const getValue = getter(myStore);
```
## withGetter
Enhances a store by adding a getter method to access its current value.
```typescript
const myStore = withGetter(writable('value'));
console.log(myStore.get()); // 'value'
```
## throttled
Creates a store that limits how often subscribers receive updates.
```typescript
const throttledStore = throttled(1000, myStore); // Updates at most once per second
```
## custom
Creates a custom store with optional throttling and custom set behavior.
```typescript
const customStore = custom(
set => {
// Setup logic
return () => {
// Cleanup logic
};
},
{ throttle: 1000 }
);
```
## adapter
Creates a derived store that can transform values between two types while maintaining two-way binding.
```typescript
const adaptedStore = adapter({
store: originalStore,
forward: (source) => /* transform to target */,
backward: (target) => /* transform back to source */
});
```
This is particularly useful when you need to transform data structures while maintaining the ability to update the original store.
+71
View File
@@ -0,0 +1,71 @@
# Collection
Utilities for creating reactive collections with automatic loading, caching, and staleness management using Svelte stores.
## Functions
### collection(options)
Creates a reactive collection that automatically loads missing items and manages freshness.
**Options:**
- `name` - Collection name for freshness tracking
- `store` - Readable store containing array of items
- `getKey` - Function to extract unique key from items
- `load` - Async function to load missing items
**Returns:**
- `indexStore` - Derived store with items indexed by key
- `deriveItem(key, relays)` - Creates a derived store for a specific item
- `loadItem(key, relays)` - Manually loads an item
- `onItem(callback)` - Subscribe to individual item updates
### makeCachedLoader(options)
Creates a cached loader function with staleness checking and exponential backoff.
**Options:**
- `name` - Loader name for freshness tracking
- `indexStore` - Store containing indexed items
- `load` - Async function to load items
- `subscribers` - Array of item update subscribers
### Freshness Management
- `getFreshness(ns, key)` - Get last update timestamp for an item
- `setFreshnessImmediate(update)` - Immediately update freshness
- `setFreshnessThrottled(update)` - Throttled freshness updates
## Example
```typescript
import {writable} from 'svelte/store'
import {derived, readable} from "svelte/store"
import {readProfile, PROFILE, PublishedProfile} from "@welshman/util"
import {Repository} from "@welshman/relay"
import {deriveEventsMapped, collection, withGetter} from "@welshman/store"
const repository = new Repository()
export const profiles = writable([])
export const {
indexStore: profilesByPubkey,
deriveItem: deriveProfile,
loadItem: loadProfile,
} = collection({
name: "profiles",
store: profiles,
getKey: profile => profile.event.pubkey,
load: (pubkey: string) => // Load the user's profile
})
// Get a reactive store for a specific profile
const hints = [/* optional relay hints to load from */]
const userProfile = deriveProfile("user-pubkey", hints)
// Subscribe to profile updates
userProfile.subscribe(profile => {
console.log("Profile updated:", profile)
})
```
+56
View File
@@ -0,0 +1,56 @@
# Custom Store
Utility for creating custom Svelte stores with start/stop lifecycle and optional throttling.
## Functions
### custom(start, options)
Creates a custom store that starts when first subscribed and stops when last subscriber unsubscribes.
**Parameters:**
- `start` - Function called when first subscriber is added. Receives a `set` function and should return an unsubscriber function
- `options` - Optional configuration object
**Options:**
- `throttle` - Throttle subscriber notifications (milliseconds)
- `onUpdate` - Callback function called when store value is set
**Returns:** WritableWithGetter store with `get()`, `set()`, `update()`, and `subscribe()` methods
## Example
```typescript
import {custom} from "@welshman/store"
// Create a store that tracks window width
const windowWidth = custom(
set => {
const updateWidth = () => set(window.innerWidth)
// Set initial value
updateWidth()
// Listen for resize events
window.addEventListener('resize', updateWidth)
// Return cleanup function
return () => window.removeEventListener('resize', updateWidth)
},
{
throttle: 100, // Throttle updates to every 100ms
onUpdate: (width) => console.log(`Window width: ${width}px`)
}
)
// Subscribe to changes
const unsubscribe = windowWidth.subscribe(width => {
console.log("Width changed:", width)
})
// Get current value
console.log("Current width:", windowWidth.get())
// Clean up
unsubscribe()
```
-122
View File
@@ -1,122 +0,0 @@
# Event-Based Stores
## deriveEventsMapped
Creates a store that maintains a mapped collection of events from a repository.
Useful when you want to transform events into a different data structure while maintaining reactivity.
```typescript
import {Repository, NAMED_PEOPLE, type TrustedEvent} from '@welshman/util'
import {deriveEventsMapped} from '@welshman/store'
interface UserProfile {
name: string;
about: string;
pubkey: string;
}
const repository = new Repository()
const profiles = deriveEventsMapped<UserProfile>(repository, {
filters: [{kinds: [PROFILE]}],
eventToItem: (event: TrustedEvent) => ({
name: event.content.name,
about: event.content.about,
pubkey: event.pubkey,
}),
itemToEvent: (profile: UserProfile) => ({
// Convert profile back to event format
kind: PROFILE,
pubkey: profile.pubkey,
content: {
name: profile.name,
about: profile.about,
}
}),
throttle: 1000, // Optional: throttle updates
includeDeleted: false // Optional: exclude deleted events
})
```
## deriveEvents
Creates a store that maintains a collection of raw events from a repository.
Useful when you want to work directly with events without transformation.
```typescript
import {Repository} from '@welshman/util'
import {deriveEvents} from '@welshman/store'
const repository = new Repository()
const textNotes = deriveEvents(repository, {
filters: [{kinds: [NOTE], // kind 1 = text note
authors: ['pubkey1', 'pubkey2']}],
throttle: 500,
includeDeleted: false
})
// Subscribe to changes
textNotes.subscribe(events => {
console.log('New text notes:', events)
})
```
## deriveEvent
Creates a store that tracks a single event by its ID or address.
Returns a derived store containing the event or undefined.
```typescript
import {Repository} from '@welshman/util'
import {deriveEvent} from '@welshman/store'
const repository = new Repository()
const specificEvent = deriveEvent(repository, 'event_id_or_address')
// Subscribe to changes of the specific event
specificEvent.subscribe(event => {
if (event) {
console.log('Event updated:', event)
} else {
console.log('Event not found')
}
})
```
## deriveIsDeleted
Creates a store that tracks whether an event has been deleted. Returns a boolean store.
```typescript
import {Repository} from '@welshman/util'
import {deriveIsDeleted} from '@welshman/store'
const repository = new Repository()
const event = /* your event */
const isDeleted = deriveIsDeleted(repository, event)
// Subscribe to deletion status changes
isDeleted.subscribe(deleted => {
console.log('Event deleted status:', deleted)
})
```
## deriveIsDeletedByAddress
Creates a store that tracks whether an event has been deleted by address.
Similar to deriveIsDeleted but checks deletion by address instead of event ID.
```typescript
import {Repository} from '@welshman/util'
import {deriveIsDeletedByAddress} from '@welshman/store'
const repository = new Repository()
const event = /* your event */
const isDeletedByAddress = deriveIsDeletedByAddress(repository, event)
// Subscribe to address-based deletion status changes
isDeletedByAddress.subscribe(deleted => {
if (deleted) {
console.log('Event has been deleted by address')
}
})
```
+55
View File
@@ -0,0 +1,55 @@
# Getter
Utilities for adding synchronous `get()` methods to Svelte stores, allowing immediate value access without subscribing. Note that this has performance implications, since it will activate a subscription that will never get unsubscribed. Do not use this on stores that require complex calculations, or which are created and destroyed.
## Functions
### getter(store)
Creates a getter function that returns the current value of a store.
**Parameters:**
- `store` - Any readable Svelte store
**Returns:** Function that returns the current store value
### withGetter(store)
Enhances a store by adding a synchronous `get()` method.
**Parameters:**
- `store` - Readable or writable Svelte store
**Returns:** Store with added `get()` method
## Types
- `ReadableWithGetter<T>` - Readable store with `get()` method
- `WritableWithGetter<T>` - Writable store with `get()` method
## Example
```typescript
import {writable, derived} from "svelte/store"
import {withGetter, getter} from "@welshman/store"
// Create enhanced stores with getter methods
const count = withGetter(writable(0))
const doubled = withGetter(derived(count, $count => $count * 2))
// Access values synchronously without subscribing
console.log(count.get()) // 0
console.log(doubled.get()) // 0
// Update the store
count.set(5)
// Get updated values immediately
console.log(count.get()) // 5
console.log(doubled.get()) // 10
// Alternative: create getter function separately
const regularStore = writable(42)
const getValue = getter(regularStore)
console.log(getValue()) // 42
```
+1
View File
@@ -1,6 +1,7 @@
# @welshman/store
[![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
+74
View File
@@ -0,0 +1,74 @@
# Repository
Reactive Svelte stores for querying and mapping events from a Repository with automatic updates.
## Functions
### deriveEventsMapped(repository, options)
Creates a reactive store that maps events to custom items and keeps them synchronized with repository updates.
**Options:**
- `filters` - Array of Nostr filters to query events
- `eventToItem` - Function to transform events to items (can return Promise)
- `itemToEvent` - Function to extract the event from an item
- `throttle?` - Throttle updates (milliseconds, default: 0)
- `includeDeleted?` - Include deleted events (default: false)
### deriveEvents(repository, options)
Creates a reactive store of events without transformation.
**Options:**
- `filters` - Array of Nostr filters
- `throttle?` - Throttle updates
- `includeDeleted?` - Include deleted events
### deriveEvent(repository, idOrAddress)
Creates a reactive store for a single event by ID or address.
### deriveIsDeleted(repository, event)
Creates a reactive store that tracks whether an event is deleted.
### deriveIsDeletedByAddress(repository, event)
Creates a reactive store that tracks whether an event is deleted by address.
## Example
```typescript
import {Repository} from "@welshman/relay"
import {deriveEventsMapped, deriveEvents} from "@welshman/store"
import {readProfile, PROFILE} from "@welshman/util"
const repository = new Repository()
// Reactive store of text notes
const textNotes = deriveEvents(repository, {
filters: [{kinds: [1], limit: 100}],
throttle: 100
})
// Reactive store of profiles mapped to custom objects
const profiles = deriveEventsMapped(repository, {
filters: [{kinds: [PROFILE]}],
eventToItem: event => readProfile(event),
itemToEvent: profile => profile.event,
includeDeleted: false
})
// Subscribe to updates
textNotes.subscribe(notes => {
console.log(`Found ${notes.length} text notes`)
})
profiles.subscribe(profiles => {
console.log(`Found ${profiles.length} profiles`)
})
// Add some events to the repository
repository.publish(someTextNoteEvent)
repository.publish(someProfileEvent)
```
+44
View File
@@ -0,0 +1,44 @@
# Synced Store
Utility for creating Svelte stores that automatically persist to and restore from localStorage.
## Functions
### synced(key, defaultValue)
Creates a writable store that synchronizes with localStorage using JSON serialization.
**Parameters:**
- `key` - localStorage key to store the value under
- `defaultValue` - Default value if nothing exists in localStorage
**Returns:** Writable Svelte store that persists changes to localStorage
The store automatically:
- Loads initial value from localStorage on creation
- Saves any changes back to localStorage
- Falls back to defaultValue if localStorage is empty or invalid
## Example
```typescript
import {synced} from "@welshman/store"
// Create a store that persists user preferences
const userPreferences = synced("user-prefs", {
theme: "dark",
notifications: true,
language: "en"
})
// Use like any writable store
userPreferences.subscribe(prefs => {
console.log("Preferences:", prefs)
})
// Update the store - automatically saves to localStorage
userPreferences.update(prefs => ({
...prefs,
theme: "light"
}))
```
+48
View File
@@ -0,0 +1,48 @@
# Throttled Store
Utility for wrapping Svelte stores to throttle subscriber notifications, reducing update frequency for performance.
## Functions
### throttled(delay, store)
Creates a throttled version of a store that limits how often subscribers are notified.
**Parameters:**
- `delay` - Throttle delay in milliseconds (0 disables throttling)
- `store` - Any readable Svelte store
**Returns:** Store with throttled subscription behavior
When `delay` is 0, returns the original store unchanged. Otherwise, wraps the store so that subscribers receive updates at most once per delay period.
## Example
```typescript
import {writable} from "svelte/store"
import {throttled} from "@welshman/store"
// Create a regular store that updates frequently
const fastStore = writable(0)
// Create a throttled version that only notifies every 100ms
const slowStore = throttled(100, fastStore)
// Subscribe to both stores
fastStore.subscribe(value => console.log("Fast:", value))
slowStore.subscribe(value => console.log("Slow:", value))
// Rapidly update the store
let count = 0
const interval = setInterval(() => {
fastStore.set(++count)
if (count >= 10) {
clearInterval(interval)
}
}, 10) // Update every 10ms
// Output:
// Fast: 1, Fast: 2, Fast: 3, ... (every update)
// Slow: 1, Slow: 5, Slow: 10 (throttled to ~100ms intervals)
```
+30
View File
@@ -0,0 +1,30 @@
# NIP-42
Utilities for NIP-42 relay authentication, allowing clients to authenticate with relays that require it.
## Functions
### makeRelayAuth(url, challenge)
Creates a CLIENT_AUTH event (kind 22242) for relay authentication as specified in NIP-42.
**Parameters:**
- `url` - The relay URL to authenticate with
- `challenge` - The challenge string provided by the relay
**Returns:** Unsigned event object with relay and challenge tags
## Example
```typescript
import {makeRelayAuth} from "@welshman/util"
// Create auth event when relay sends AUTH challenge
const authEvent = makeRelayAuth(
"wss://relay.example.com",
"challenge-string-from-relay"
)
// Sign the event with your signer
const signedAuth = await signer.sign(authEvent)
```
-86
View File
@@ -70,89 +70,3 @@ export declare const displayRelayUrl: (url: string) => string;
// Get display name for relay profile
export declare const displayRelayProfile: (profile?: RelayProfile, fallback?: string) => string;
```
## Examples
### URL Validation
```typescript
import {
isRelayUrl,
isOnionUrl,
isLocalUrl,
isShareableRelayUrl
} from '@welshman/util';
// Valid relay URLs
console.log(isRelayUrl('wss://relay.damus.io')); // true
console.log(isRelayUrl('relay.damus.io')); // true (auto-adds wss://)
console.log(isRelayUrl('ws://localhost:8080')); // true
// Invalid URLs
console.log(isRelayUrl('https://example.com')); // false (not websocket)
console.log(isRelayUrl('invalid-url')); // false
// Special URL types
console.log(isOnionUrl('wss://7rqsrjfmyb3n2k72.onion')); // true
console.log(isLocalUrl('ws://localhost:8080')); // true
console.log(isLocalUrl('wss://relay.local')); // true
// Safe to share publicly
console.log(isShareableRelayUrl('wss://relay.damus.io')); // true
console.log(isShareableRelayUrl('ws://localhost:8080')); // false (local)
```
### URL Normalization
```typescript
import { normalizeRelayUrl, displayRelayUrl } from '@welshman/util';
// Normalize various URL formats
console.log(normalizeRelayUrl('relay.damus.io'));
// 'wss://relay.damus.io/'
console.log(normalizeRelayUrl('ws://RELAY.EXAMPLE.COM/path'));
// 'ws://relay.example.com/path'
console.log(normalizeRelayUrl('wss://relay.damus.io/?ref=123'));
// 'wss://relay.damus.io/' (strips query params)
// Format for display
console.log(displayRelayUrl('wss://relay.damus.io/'));
// 'relay.damus.io'
console.log(displayRelayUrl('ws://localhost:8080/'));
// 'localhost:8080'
```
### Working with Relay Profiles
```typescript
import { displayRelayProfile, RelayProfile } from '@welshman/util';
const relayProfile: RelayProfile = {
url: 'wss://relay.damus.io',
name: 'Damus Relay',
description: 'A high-performance Nostr relay',
software: 'strfry',
version: '1.0.0',
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
limitation: {
payment_required: false,
auth_required: false,
min_pow_difficulty: 0
}
};
// Get display name
const displayName = displayRelayProfile(relayProfile);
console.log(displayName); // 'Damus Relay'
// With fallback for unnamed relays
const anonymousRelay: RelayProfile = {
url: 'wss://anonymous.relay.com'
};
const name = displayRelayProfile(anonymousRelay, 'Unknown Relay');
console.log(name); // 'Unknown Relay'
```
-115
View File
@@ -1,115 +0,0 @@
# Repository
The Repository module provides a robust in-memory event storage system with indexing, querying, and event replacement capabilities.
## Core Features
- Event storage and indexing
- Query support with multiple filters
- Event replacement and deletion tracking
- Event update notifications
- Optimized indexes for common queries
## Class Definition
```typescript
class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
// Storage indexes
eventsById = new Map<string, E>()
eventsByWrap = new Map<string, E>()
eventsByAddress = new Map<string, E>()
eventsByTag = new Map<string, E[]>()
eventsByDay = new Map<number, E[]>()
eventsByAuthor = new Map<string, E[]>()
eventsByKind = new Map<number, E[]>()
deletes = new Map<string, number>()
}
```
## Core Methods
### Event Management
```typescript
// Store or update event
publish(event: E, opts = { shouldNotify: true }): boolean
// Get event by ID or address
getEvent(idOrAddress: string): E | undefined
// Check if event exists
hasEvent(event: E): boolean
// Remove event
removeEvent(idOrAddress: string): void
// Check deletion status
isDeleted(event: E): boolean
isDeletedByAddress(event: E): boolean
isDeletedById(event: E): boolean
```
### Querying
```typescript
// Query events with filters
query(
filters: Filter[],
opts = {
includeDeleted: false,
shouldSort: true
}
): E[]
// Dump all events
dump(): E[]
// Load events in bulk
load(events: E[], chunkSize = 1000): void
```
## Usage Examples
### Basic Repository Operations
```typescript
// Create repository
const repo = new Repository<TrustedEvent>()
// Add events
repo.publish(event)
// Query events
const events = repo.query([
{ kinds: [1], limit: 100 }
])
// Check event status
if (!repo.isDeleted(event)) {
processEvent(event)
}
```
### Bulk Operations
```typescript
// Load multiple events
repo.load(events, 500) // Process in chunks of 500
// Get all events
const allEvents = repo.dump()
```
### Query Examples
```typescript
// Query with multiple filters
const events = repo.query([
// Recent events from specific authors
{
kinds: [1],
authors: ['pub1', 'pub2'],
since: now() - 24 * 60 * 60
},
// Events with specific tags
{
'#t': ['bitcoin', 'nostr'],
limit: 50
}
])
```
+2 -2
View File
@@ -26,7 +26,7 @@ export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {
return () => repository.off("update", onUpdate)
},
{
set: (other: Repository) => repository.load(other.dump()),
onUpdate: (other: Repository) => repository.load(other.dump()),
},
)
@@ -53,6 +53,6 @@ export const makeTrackerStore = ({throttle: t = 300}: {throttle?: number} = {})
}
},
{
set: (other: Tracker) => tracker.load(other.relaysById),
onUpdate: (other: Tracker) => tracker.load(other.relaysById),
},
)
+3 -3
View File
@@ -6,7 +6,7 @@ type Start<T> = (set: Subscriber<T>) => Unsubscriber
export type CustomStoreOpts<T> = {
throttle?: number
set?: (x: T) => void
onUpdate?: (x: T) => void
}
export const custom = <T>(
@@ -30,13 +30,13 @@ export const custom = <T>(
get: () => value,
set: (newValue: T) => {
set(newValue)
opts.set?.(newValue)
opts.onUpdate?.(newValue)
},
update: (f: (value: T) => T) => {
const newValue = f(value)
set(newValue)
opts.set?.(newValue)
opts.onUpdate?.(newValue)
},
subscribe: (sub: Subscriber<T>) => {
if (opts.throttle) {