diff --git a/.fdignore b/.fdignore index 603fd55..691ae4c 100644 --- a/.fdignore +++ b/.fdignore @@ -1,5 +1,5 @@ node_modules -#docs +docs docs/reference docs/.vitepress/cache build diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index eb21a5d..66bd238 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -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"}, ], }, ], diff --git a/docs/relay/index.md b/docs/relay/index.md index 1afa062..150291d 100644 --- a/docs/relay/index.md +++ b/docs/relay/index.md @@ -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 diff --git a/docs/router/index.md b/docs/router/index.md index ef514e9..8afc703 100644 --- a/docs/router/index.md +++ b/docs/router/index.md @@ -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. diff --git a/docs/signer/nip-46.md b/docs/signer/nip-46.md index 792d252..bcccbcb 100644 --- a/docs/signer/nip-46.md +++ b/docs/signer/nip-46.md @@ -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 -broker.getPublicKey(): Promise -broker.connect(connectSecret?: string, perms?: string): Promise - -// Signing and encryption -broker.signEvent(event: StampedEvent): Promise -broker.nip04Encrypt(pk: string, message: string): Promise -broker.nip04Decrypt(pk: string, message: string): Promise -broker.nip44Encrypt(pk: string, message: string): Promise -broker.nip44Decrypt(pk: string, message: string): Promise -``` - ## Nip46Signer Usage ```typescript diff --git a/docs/store/basic.md b/docs/store/basic.md deleted file mode 100644 index 0192299..0000000 --- a/docs/store/basic.md +++ /dev/null @@ -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. diff --git a/docs/store/collection.md b/docs/store/collection.md new file mode 100644 index 0000000..72130fd --- /dev/null +++ b/docs/store/collection.md @@ -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) +}) +``` diff --git a/docs/store/custom.md b/docs/store/custom.md new file mode 100644 index 0000000..5125e41 --- /dev/null +++ b/docs/store/custom.md @@ -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() +``` diff --git a/docs/store/events.md b/docs/store/events.md deleted file mode 100644 index 19ab5fa..0000000 --- a/docs/store/events.md +++ /dev/null @@ -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(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') - } -}) -``` diff --git a/docs/store/getter.md b/docs/store/getter.md new file mode 100644 index 0000000..5b172d8 --- /dev/null +++ b/docs/store/getter.md @@ -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` - Readable store with `get()` method +- `WritableWithGetter` - 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 +``` diff --git a/docs/store/index.md b/docs/store/index.md index 7a0c133..0ab91c8 100644 --- a/docs/store/index.md +++ b/docs/store/index.md @@ -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 diff --git a/docs/store/repository.md b/docs/store/repository.md new file mode 100644 index 0000000..5e73c35 --- /dev/null +++ b/docs/store/repository.md @@ -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) +``` \ No newline at end of file diff --git a/docs/store/synced.md b/docs/store/synced.md new file mode 100644 index 0000000..f726af1 --- /dev/null +++ b/docs/store/synced.md @@ -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" +})) +``` diff --git a/docs/store/throttle.md b/docs/store/throttle.md new file mode 100644 index 0000000..69fdcb4 --- /dev/null +++ b/docs/store/throttle.md @@ -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) +``` diff --git a/docs/util/nip42.md b/docs/util/nip42.md new file mode 100644 index 0000000..fe36850 --- /dev/null +++ b/docs/util/nip42.md @@ -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) +``` diff --git a/docs/util/relay.md b/docs/util/relay.md index fcd4d63..9708076 100644 --- a/docs/util/relay.md +++ b/docs/util/relay.md @@ -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' -``` diff --git a/docs/util/repository.md b/docs/util/repository.md deleted file mode 100644 index a373bb7..0000000 --- a/docs/util/repository.md +++ /dev/null @@ -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 extends Emitter { - // Storage indexes - eventsById = new Map() - eventsByWrap = new Map() - eventsByAddress = new Map() - eventsByTag = new Map() - eventsByDay = new Map() - eventsByAuthor = new Map() - eventsByKind = new Map() - deletes = new Map() -} -``` - -## 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() - -// 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 - } -]) -``` diff --git a/packages/app/src/core.ts b/packages/app/src/core.ts index 8541e2b..797860b 100644 --- a/packages/app/src/core.ts +++ b/packages/app/src/core.ts @@ -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), }, ) diff --git a/packages/store/src/custom.ts b/packages/store/src/custom.ts index c156377..6196727 100644 --- a/packages/store/src/custom.ts +++ b/packages/store/src/custom.ts @@ -6,7 +6,7 @@ type Start = (set: Subscriber) => Unsubscriber export type CustomStoreOpts = { throttle?: number - set?: (x: T) => void + onUpdate?: (x: T) => void } export const custom = ( @@ -30,13 +30,13 @@ export const custom = ( 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) => { if (opts.throttle) {