Delete readmes, update docs
This commit is contained in:
@@ -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
|
||||
|
||||
+62
-60
@@ -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"},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# Collection Stores
|
||||
|
||||
The `collection` utility creates stores that handle caching, loading, and indexing of Nostr data. It provides a consistent pattern for managing entities that need to be fetched from the network and cached locally.
|
||||
|
||||
```typescript
|
||||
const {
|
||||
indexStore, // Map of all items by key
|
||||
deriveItem, // Get reactive item by key
|
||||
loadItem // Trigger network load
|
||||
} = collection({
|
||||
name: "storeName", // For persistence
|
||||
store: writable([]), // Base store
|
||||
getKey: item => item.id // How to index items
|
||||
load: async (key) => { // Network loader
|
||||
// Load logic here
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Available Collections
|
||||
|
||||
```typescript
|
||||
// Profiles
|
||||
profiles → profilesByPubkey → deriveProfile → loadProfile
|
||||
|
||||
// Lists
|
||||
follows → followsByPubkey → deriveFollows → loadFollows
|
||||
mutes → mutesByPubkey → deriveMutes → loadMutes
|
||||
pins → pinsByPubkey → derivePins → loadPins
|
||||
|
||||
// Relays
|
||||
relays → relaysByUrl → deriveRelay → loadRelay
|
||||
relaySelections → relaySelectionsByPubkey → deriveRelaySelections → loadRelaySelections
|
||||
inboxRelaySelections → inboxRelaySelectionsByPubkey → deriveInboxRelaySelections → loadInboxRelaySelections
|
||||
|
||||
// Identity
|
||||
handles → handlesByNip05 → deriveHandle → loadHandle
|
||||
zappers → zappersByLnurl → deriveZapper → loadZapper
|
||||
```
|
||||
|
||||
## Real World Examples
|
||||
|
||||
### Loading and Displaying Profiles
|
||||
|
||||
```typescript
|
||||
import {
|
||||
deriveProfile,
|
||||
loadProfile,
|
||||
displayProfile
|
||||
} from '@welshman/app'
|
||||
|
||||
// In a Svelte component
|
||||
let profile
|
||||
|
||||
// Subscribe to profile changes
|
||||
$: profile = $deriveProfile(pubkey)
|
||||
|
||||
// Load automatically triggers when needed
|
||||
onMount(() => {
|
||||
loadProfile(pubkey, {
|
||||
// Optional request params
|
||||
relays: ["wss://relay.example.com"]
|
||||
})
|
||||
})
|
||||
|
||||
// Display with fallback
|
||||
$: name = displayProfile(profile, "unknown")
|
||||
```
|
||||
|
||||
### Managing Relay Selections
|
||||
|
||||
```typescript
|
||||
import {
|
||||
deriveRelaySelections,
|
||||
loadRelaySelections,
|
||||
getReadRelayUrls,
|
||||
getWriteRelayUrls
|
||||
} from '@welshman/app'
|
||||
|
||||
// Get user's relay preferences
|
||||
const selections = deriveRelaySelections(pubkey).get()
|
||||
|
||||
// Load from network if needed
|
||||
await loadRelaySelections(pubkey)
|
||||
|
||||
// Get read/write URLs
|
||||
const readRelays = getReadRelayUrls(selections)
|
||||
const writeRelays = getWriteRelayUrls(selections)
|
||||
|
||||
// Use with router
|
||||
const relays = ctx.app.router
|
||||
.FromPubkey(pubkey)
|
||||
.getUrls()
|
||||
```
|
||||
|
||||
Each collection automatically:
|
||||
- Caches to IndexedDB
|
||||
- Deduplicates network requests
|
||||
- Updates reactively
|
||||
- Provides typed access
|
||||
- Handles loading states
|
||||
|
||||
The pattern is consistent across all stores, making it predictable to work with different types of nostr data.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Commands
|
||||
|
||||
High-level commands for common Nostr operations.
|
||||
Each command handles signing, encryption, and relay selection automatically.
|
||||
|
||||
## Available Commands
|
||||
|
||||
```typescript
|
||||
// List Management
|
||||
follow(pubkey)
|
||||
unfollow(pubkey)
|
||||
mute(pubkey)
|
||||
unmute(pubkey)
|
||||
pin(tag)
|
||||
unpin(tag)
|
||||
```
|
||||
|
||||
Each command returns a [`Thunk`](app/thunk) which:
|
||||
- Optimistically updates local state
|
||||
- Signs and publishes the event
|
||||
- Can be aborted within a delay window
|
||||
- Reports publish progress
|
||||
|
||||
## Real World Examples
|
||||
|
||||
### Following/Unfollowing Users
|
||||
|
||||
```typescript
|
||||
import {follow, unfollow, userFollows} from '@welshman/app'
|
||||
|
||||
// Follow with optimistic update
|
||||
const followUser = async (pubkey: string) => {
|
||||
|
||||
// Creates and publishes event with an updated follow list
|
||||
const thunk = await follow(pubkey)
|
||||
|
||||
// Track publish status per relay
|
||||
thunk.status.subscribe(statuses => {
|
||||
for (const [url, status] of Object.entries(statuses)) {
|
||||
console.log(`${url}: ${status}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Can abort within delay window
|
||||
setTimeout(() => thunk.controller.abort(), 1000)
|
||||
}
|
||||
|
||||
// Unfollow works the same way
|
||||
const unfollowUser = async (pubkey: string) => {
|
||||
const thunk = await unfollow(pubkey)
|
||||
|
||||
// Wait for completion
|
||||
const results = await thunk.result
|
||||
}
|
||||
```
|
||||
|
||||
### Managing Pins
|
||||
|
||||
```typescript
|
||||
import {pin, unpin, userPins} from '@welshman/app'
|
||||
|
||||
// Pin an event with context
|
||||
const pinEvent = async (event: TrustedEvent) => {
|
||||
const thunk = await pin([
|
||||
'e', event.id,
|
||||
ctx.app.router.Event(event).getUrl()
|
||||
])
|
||||
|
||||
// Handle specific relay errors
|
||||
thunk.status.subscribe(statuses => {
|
||||
for (const [url, {status, message}] of Object.entries(statuses)) {
|
||||
if (status === 'failure') {
|
||||
console.error(`Failed on ${url}: ${message}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
All commands:
|
||||
- Handle encryption automatically
|
||||
- Select appropriate relays
|
||||
- Update local state immediately
|
||||
- Allow soft-undo via abort
|
||||
- Report per-relay status
|
||||
- Return consistent Thunk interface
|
||||
|
||||
Commands provide a high-level way to modify the Nostr state without dealing with the complexities of event creation, encryption, and relay selection.
|
||||
+6
-132
@@ -1,139 +1,13 @@
|
||||
# Application Context
|
||||
|
||||
The `@welshman/app` package uses a global context system to configure core behaviors.
|
||||
Understanding the app context is essential as it powers [session/authentication](/app/session), [relay routing](/app/relay) and [request handling](/app/request).
|
||||
The `@welshman/app` package uses a global context system to configure a few core behaviors.
|
||||
|
||||
## Basic Setup
|
||||
## Dufflepud
|
||||
|
||||
[Dufflepud](https://github.com/coracle-social/dufflepud) is a utility server that can retrieve NIP 05 profiles, zappers, relay metadata, link previews, etc. It's not necessary for using welshman, but can improve things by bypassing CORS.
|
||||
|
||||
```typescript
|
||||
import {ctx, setContext} from '@welshman/lib'
|
||||
import {getDefaultNetContext, getDefaultAppContext} from '@welshman/app'
|
||||
import {appContext} from '@welshman/app'
|
||||
|
||||
// Initialize app with default settings
|
||||
setContext({
|
||||
net: getDefaultNetContext(),
|
||||
app: getDefaultAppContext()
|
||||
})
|
||||
|
||||
// Access context anywhere
|
||||
console.log(ctx.app.router)
|
||||
console.log(ctx.net.pool)
|
||||
```
|
||||
|
||||
## Default App Context
|
||||
|
||||
```typescript
|
||||
export type AppContext = {
|
||||
// Smart relay routing system
|
||||
router: Router
|
||||
|
||||
// Time to wait between batched requests (ms)
|
||||
requestDelay: number // default: 50
|
||||
|
||||
// Time to wait for NIP-42 relay auth (ms)
|
||||
authTimeout: number // default: 300
|
||||
|
||||
// Time to wait for request completion (ms)
|
||||
requestTimeout: number // default: 3000
|
||||
|
||||
// URL of metadata service (optional)
|
||||
dufflepudUrl?: string
|
||||
|
||||
// Additional relays for indexed content
|
||||
indexerRelays?: string[]
|
||||
}
|
||||
|
||||
// Example with custom settings
|
||||
setContext({
|
||||
app: getDefaultAppContext({
|
||||
requestDelay: 100,
|
||||
authTimeout: 500,
|
||||
requestTimeout: 5000,
|
||||
dufflepudUrl: "https://api.example.com",
|
||||
indexerRelays: [
|
||||
"wss://relay.example.com",
|
||||
"wss://indexed.example.com"
|
||||
]
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Network Context
|
||||
|
||||
```typescript
|
||||
export type NetContext = {
|
||||
// Global connection pool
|
||||
pool: Pool
|
||||
|
||||
// How to handle NIP-42 auth
|
||||
authMode: AuthMode // default: 'implicit'
|
||||
|
||||
// Event validation and handling
|
||||
onEvent: (url: string, event: TrustedEvent) => void
|
||||
isDeleted: (url: string, event: TrustedEvent) => boolean
|
||||
isValid: (url: string, event: TrustedEvent) => boolean
|
||||
|
||||
// Event signing (used by all packages)
|
||||
signEvent: (event: StampedEvent) => Promise<SignedEvent>
|
||||
|
||||
// Subscription optimization
|
||||
optimizeSubscriptions: (subs: Subscription[]) => RelaysAndFilters[]
|
||||
}
|
||||
|
||||
// Example with custom validation
|
||||
setContext({
|
||||
net: getDefaultNetContext({
|
||||
// Custom event validation
|
||||
isValid: (url, event) => {
|
||||
if (url === LOCAL_RELAY_URL) return true
|
||||
return hasValidSignature(event)
|
||||
},
|
||||
|
||||
// Track deleted events
|
||||
isDeleted: (url, event) =>
|
||||
repository.isDeleted(event),
|
||||
|
||||
// Custom event handling
|
||||
onEvent: (url, event) => {
|
||||
// Save to local repository
|
||||
repository.publish(event)
|
||||
|
||||
// Track which relay it came from
|
||||
tracker.track(event.id, url)
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Using Context Values
|
||||
|
||||
Once configured, context values are used throughout the app:
|
||||
|
||||
```typescript
|
||||
import {ctx} from '@welshman/lib'
|
||||
|
||||
// Smart relay routing
|
||||
const relays = ctx.app.router
|
||||
.ForPubkey(pubkey)
|
||||
.getUrls()
|
||||
|
||||
// Publish with timeout
|
||||
const pub = publish({
|
||||
event,
|
||||
relays,
|
||||
timeout: ctx.app.requestTimeout
|
||||
})
|
||||
|
||||
// Subscribe with auth
|
||||
const sub = subscribe({
|
||||
filters,
|
||||
relays,
|
||||
authTimeout: ctx.app.authTimeout
|
||||
})
|
||||
|
||||
// Check connection pool
|
||||
const connected = ctx.net.pool
|
||||
.get(relay)
|
||||
.socket.status === 'open'
|
||||
appContext.dufflepudUrl = 'https://my-dufflepud-instance.com'
|
||||
```
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# Feed
|
||||
|
||||
The feed system provides a powerful way to compose and load complex `Nostr` queries. It supports user scopes, web of trust filtering, DVM integration, and thread construction.
|
||||
|
||||
## Controller
|
||||
|
||||
The `controller.load()` function is the main interface for fetching events from a feed. It handles all the complexity of relay selection, subscription management, and event filtering.
|
||||
|
||||
```typescript
|
||||
import {createFeedController} from '@welshman/app'
|
||||
import {scopeFeed, wotFeed} from '@welshman/feeds'
|
||||
|
||||
const controller = createFeedController({
|
||||
// Define what to load
|
||||
feed: scopeFeed("follows"),
|
||||
|
||||
// Optional configurations
|
||||
closeOnEose: true, // Close after getting all events
|
||||
onEvent: event => {}, // Handle events as they arrive
|
||||
onEose: url => {}, // Handle EOSE from each relay
|
||||
onComplete: () => {}, // Called when all relays complete
|
||||
})
|
||||
|
||||
// Load first 20 events
|
||||
const events = await controller.load(20)
|
||||
|
||||
// Load next 20 events
|
||||
const moreEvents = await controller.load(20)
|
||||
```
|
||||
|
||||
The controller maintains its state between loads, so subsequent calls will:
|
||||
- Continue from last position
|
||||
- Use appropriate time windows
|
||||
- Skip already seen events
|
||||
- Maintain relay connections
|
||||
|
||||
## Paginated Feed
|
||||
|
||||
```typescript
|
||||
import {intersectionFeed, scopeFeed, wotFeed} from '@welshman/feeds'
|
||||
|
||||
const HomeFeed = {
|
||||
let events = []
|
||||
let loading = false
|
||||
let controller
|
||||
|
||||
onMount(() => {
|
||||
// Create feed for home timeline
|
||||
controller = createFeedController({
|
||||
feed: intersectionFeed(
|
||||
// Content from follows
|
||||
scopeFeed("follows"),
|
||||
// Filtered by web of trust
|
||||
wotFeed({min: 0.1})
|
||||
),
|
||||
|
||||
// Handle events as they arrive
|
||||
onEvent: event => {
|
||||
events = [...events, event]
|
||||
},
|
||||
|
||||
// Track loading state
|
||||
onComplete: () => {
|
||||
loading = false
|
||||
}
|
||||
})
|
||||
|
||||
// Initial load
|
||||
loadMore()
|
||||
})
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading) return
|
||||
loading = true
|
||||
|
||||
// Load next batch
|
||||
await controller.load(20)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points about `controller.load()`:
|
||||
- Takes a limit parameter for batch size
|
||||
- Returns a promise of loaded events
|
||||
- Can be called repeatedly for pagination
|
||||
- Handles subscription lifecycle
|
||||
- Manages relay connections
|
||||
- Deduplicates events
|
||||
|
||||
The controller is stateful and maintains:
|
||||
- Current time window
|
||||
- Seen events
|
||||
- Active subscriptions
|
||||
- Relay connections
|
||||
|
||||
This makes it ideal for implementing infinite scroll feeds, thread loading, and other paginated content scenarios.
|
||||
+63
-1
@@ -1,5 +1,7 @@
|
||||
# @welshman/app
|
||||
|
||||
[](https://npmjs.com/package/@welshman/app)
|
||||
|
||||
A comprehensive framework for building nostr clients, powering production applications like [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social). It provides a complete toolkit for managing events, subscriptions, user data, and relay connections.
|
||||
|
||||
## What's Included
|
||||
@@ -7,11 +9,71 @@ A comprehensive framework for building nostr clients, powering production applic
|
||||
- **Repository System** - Event storage and query capabilities
|
||||
- **Router** - Intelligent relay selection for optimal networking
|
||||
- **Feed Controller** - Manages feed creation and updates
|
||||
- **Authentication** - User identity and key management
|
||||
- **Session Management** - User identity and key management
|
||||
- **Event Actions** - High-level operations like reacting, replying, etc.
|
||||
- **Profile Management** - User profile handling and metadata
|
||||
- **Relay Directories** - Discovery and management of relays
|
||||
- **Web of Trust** - Utilities for building webs of trust
|
||||
|
||||
## Quick Example
|
||||
|
||||
```typescript
|
||||
import {getNip07} from '@welshman/signer'
|
||||
import {load, request, RequestEvent, defaultSocketPolicies, makeSocketPolicyAuth, Socket} from '@welshman/net'
|
||||
import {StampedEvent, TrustedEvent, makeEvent, NOTE} from '@welshman/util'
|
||||
import {pubkey, signer, publishThunk} from '@welshman/app'
|
||||
|
||||
// Log in via NIP 07
|
||||
addSession({method: 'nip07', pubkey: await getNip07().getPubkey()})
|
||||
|
||||
// Enable automatic authentication to relays
|
||||
defaultSocketPolicies.push(
|
||||
makeSocketPolicyAuth({
|
||||
sign: (event: StampedEvent) => signer.get()?.sign(event),
|
||||
shouldAuth: (socket: Socket) => true,
|
||||
}),
|
||||
)
|
||||
|
||||
// This will fetch the user's profile automatically, and return a store that updates
|
||||
// automatically. Several different stores exist that are ready to go, including handles,
|
||||
// zappers, relaySelections, relays, follows, mutes.
|
||||
const profile = deriveProfile(pubkey.get())
|
||||
|
||||
// Publish is done using thunks, which optimistically publish to the local database, deferring
|
||||
// signing and publishing for instant user feedback. Progress is reported as relays accept/reject the event
|
||||
// Events are automatically signed using the current session
|
||||
const thunk = publishThunk({
|
||||
relays: Router.get().FromUser().getUrls(),
|
||||
event: makeEvent(NOTE, {content: "hi"}),
|
||||
delay: 3000,
|
||||
})
|
||||
|
||||
// Thunks can be aborted until after `delay`, allowing for soft-undo
|
||||
thunk.controller.abort()
|
||||
|
||||
// Some commands are included
|
||||
const thunk = follow('97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322')
|
||||
|
||||
// Load events as a promise
|
||||
const events = await load({
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
}])
|
||||
|
||||
// Or use `request` for more fine-grained subscription control
|
||||
const req = request({
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
}])
|
||||
|
||||
// Listen for events
|
||||
req.on(RequestEvent.Event, (event: TrustedEvent) => {
|
||||
console.log(event)
|
||||
})
|
||||
|
||||
// Close the req
|
||||
req.close()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Making Requests
|
||||
|
||||
Welshman extends Nostr's base subscription model with intelligent caching, repository integration, and configurable behaviors.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Local Repository**: Events are automatically cached and tracked
|
||||
- **Cache Intelligence**: Smart decisions about when to use cached data
|
||||
- **Relay Integration**: Works with the router for optimal relay selection
|
||||
- **Configurable Behavior**: Control caching and timeouts
|
||||
|
||||
## Request and Load
|
||||
|
||||
The base functionality for subscription management is implemented in `@welshman/net`. Please refer to [the documentation](/net) for that module for details.
|
||||
|
||||
## Collections and Loaders
|
||||
|
||||
The `collection` utility creates stores that handle caching, loading, and indexing of Nostr data. It provides a consistent pattern for managing entities that need to be fetched from the network and cached locally.
|
||||
|
||||
```typescript
|
||||
const {
|
||||
indexStore, // Map of all items by key
|
||||
deriveItem, // Get reactive item by key
|
||||
loadItem // Trigger network load
|
||||
} = collection({
|
||||
name: "storeName", // For persistence
|
||||
store: writable([]), // Base store
|
||||
getKey: item => item.id // How to index items
|
||||
load: async (key) => { // Network loader
|
||||
// Load logic here
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Deriving Events
|
||||
|
||||
The best way to create collections is by deriving their contents from the app `repository` using `deriveEvents` from `@welshman/store`. For more control, use `deriveEventsMapped`.
|
||||
|
||||
```typescript
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
|
||||
export const notes = deriveEvents<TrustedEvent>(repository, {filters: [{kinds: [NOTE]}]})
|
||||
```
|
||||
|
||||
A collection could then be created by passing the `notes` store to `collection`.
|
||||
|
||||
### Available Collections
|
||||
|
||||
Several common collections are built-in and ready for use:
|
||||
|
||||
```typescript
|
||||
// Profiles
|
||||
profiles → profilesByPubkey → deriveProfile → loadProfile
|
||||
|
||||
// Lists
|
||||
follows → followsByPubkey → deriveFollows → loadFollows
|
||||
mutes → mutesByPubkey → deriveMutes → loadMutes
|
||||
pins → pinsByPubkey → derivePins → loadPins
|
||||
|
||||
// Relays
|
||||
relays → relaysByUrl → deriveRelay → loadRelay
|
||||
relaySelections → relaySelectionsByPubkey → deriveRelaySelections → loadRelaySelections
|
||||
inboxRelaySelections → inboxRelaySelectionsByPubkey → deriveInboxRelaySelections → loadInboxRelaySelections
|
||||
|
||||
// Identity
|
||||
handles → handlesByNip05 → deriveHandle → loadHandle
|
||||
zappers → zappersByLnurl → deriveZapper → loadZapper
|
||||
```
|
||||
|
||||
### Example - Loading and Displaying Profiles
|
||||
|
||||
```typescript
|
||||
import {get} from 'svelte/store'
|
||||
import {displayProfile} from '@welshman/util'
|
||||
import {deriveProfile, deriveProfileDisplay} from '@welshman/app'
|
||||
|
||||
// Subscribe to profile changes - this will automatically load the profile in the background
|
||||
const profile = deriveProfile(pubkey)
|
||||
|
||||
// Display with fallback
|
||||
const name = displayProfile(get(profile), 'unknown')
|
||||
|
||||
// Better: use built-in deriveProfileDisplay utility
|
||||
const name = deriveProfileDisplay(pubkey)
|
||||
```
|
||||
|
||||
### User-Specific Collections
|
||||
|
||||
Several modules provide user-specific derived stores that automatically load data for the currently signed-in user:
|
||||
|
||||
```typescript
|
||||
import { userProfile, userFollows, userMutes, userPins } from '@welshman/app'
|
||||
|
||||
userProfile.subscribe(profile => {
|
||||
// Current user's profile data
|
||||
})
|
||||
|
||||
userFollows.subscribe(follows => {
|
||||
// Current user's follow list
|
||||
})
|
||||
```
|
||||
|
||||
### Repository Integration
|
||||
|
||||
All events from subscriptions are automatically:
|
||||
|
||||
- Saved to the repository
|
||||
- Tracked to their source relay
|
||||
- Checked against deletion status
|
||||
|
||||
The repository serves as an intelligent cache layer, making subsequent queries for the same data faster.
|
||||
|
||||
## Feeds
|
||||
|
||||
A high-level feed loader utility is also provided, which combines application state with utilities from `@welshman/net` and `@welshman/feeds`.
|
||||
|
||||
```typescript
|
||||
import {NOTE} from '@welshman/util'
|
||||
import {makeKindFeed} from '@welshman/feeds'
|
||||
import {createFeedController} from '@welshman/app'
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
let done = false
|
||||
|
||||
const ctrl = createFeedController({
|
||||
feed: makeKindFeed(NOTE),
|
||||
useWindowing: true,
|
||||
signal: abortController.signal,
|
||||
onEvent: e => {
|
||||
console.log(e)
|
||||
},
|
||||
onExhausted: () => {
|
||||
done = true
|
||||
},
|
||||
})
|
||||
|
||||
// Load some notes
|
||||
ctrl.load(100)
|
||||
|
||||
// Cancel any pending requests
|
||||
abortController.abort()
|
||||
```
|
||||
@@ -5,9 +5,10 @@ Thunks provide optimistic updates for event publishing. They immediately update
|
||||
## Overview
|
||||
|
||||
A thunk:
|
||||
|
||||
- Updates local state immediately
|
||||
- Handles event signing in the background
|
||||
- Manages publish status per relay
|
||||
- Handles event signing in the background using the current session
|
||||
- Tracks publish status per relay
|
||||
- Supports soft-undo via abort
|
||||
- Can be delayed/cancelled
|
||||
- Tracks successful publishes
|
||||
@@ -46,7 +47,17 @@ const publish = async (content: string) => {
|
||||
}, 1000)
|
||||
|
||||
// Wait for completion
|
||||
const results = await thunk.result
|
||||
return results
|
||||
await thunk.result
|
||||
}
|
||||
```
|
||||
|
||||
## Built in commands
|
||||
|
||||
Several thunk factories are provided for more complicated scenarios like updating lists:
|
||||
|
||||
- `follow(pubkey)`
|
||||
- `unfollow(pubkey)`
|
||||
- `mute(pubkey)`
|
||||
- `unmute(pubkey)`
|
||||
- `pin(tag)`
|
||||
- `unpin(tag)`
|
||||
@@ -0,0 +1,56 @@
|
||||
# Router
|
||||
|
||||
The Welshman router can be used to enable the `outbox model` in your Nostr application. It handles relay selection for reading, writing, and discovering events while considering relay quality, user preferences, and network conditions.
|
||||
|
||||
## Overview
|
||||
|
||||
The router provides scenarios for common **Nostr** operations:
|
||||
|
||||
- Reading user profiles
|
||||
- Publishing events
|
||||
- Following threads
|
||||
- Handling DMs
|
||||
- Searching content
|
||||
|
||||
Each scenario considers:
|
||||
|
||||
- User's relay preferences (NIP-65)
|
||||
- Event hints in tags
|
||||
- Relay quality scores
|
||||
- Fallback policies
|
||||
- Connection status
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import {routerContext, addMaximalFallbacks, Router} from '@welshman/app'
|
||||
|
||||
// Set up global router options
|
||||
routerContext.getDefaultRelays = () => ["wss://relay.damus.io/", "wss://nos.lol/"]
|
||||
|
||||
// Router can be used directly with options, or via a singleton with global options
|
||||
const router = Router.get()
|
||||
|
||||
// Get relays for reading a profile
|
||||
const readRelays = router.ForPubkey(pubkey).getUrls()
|
||||
|
||||
// Get relays for broadcasting events by the current user
|
||||
const writeRelays = router.FromUser().getUrls()
|
||||
|
||||
// Get relays for a quote
|
||||
const quoteRelays = Router.get()
|
||||
.Quote(parentEvent, idOrAddress, relayHints)
|
||||
.policy(addMaximalFallbacks)
|
||||
.getUrls()
|
||||
|
||||
```
|
||||
|
||||
## Router Features
|
||||
|
||||
- Smart relay selection based on relay monitoring
|
||||
- Quality scoring of relays
|
||||
- Fallback strategies
|
||||
- Handling of special relay types (.onion, local)
|
||||
- NIP-65 support
|
||||
|
||||
The router is central to efficient nostr operations, ensuring events reach their intended audience while minimizing unnecessary network traffic.
|
||||
@@ -1,103 +0,0 @@
|
||||
# Router
|
||||
|
||||
The Router is the critical component to efficiently enable the `outbox model` in your Nostr application. It handles relay selection for reading, writing, and discovering events while considering relay quality, user preferences, and network conditions.
|
||||
|
||||
## Overview
|
||||
|
||||
The router provides scenarios for common **Nostr** operations:
|
||||
- Reading user profiles
|
||||
- Publishing events
|
||||
- Following threads
|
||||
- Handling DMs
|
||||
- Searching content
|
||||
|
||||
Each scenario considers:
|
||||
- User's relay preferences (NIP-65)
|
||||
- Event hints in tags
|
||||
- Relay quality scores
|
||||
- Fallback policies
|
||||
- Connection status
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import {ctx, setContext} from '@welshman/lib'
|
||||
import {getDefaultAppContext} from '@welshman/app'
|
||||
|
||||
// Initialize router
|
||||
setContext({
|
||||
app: getDefaultAppContext()
|
||||
})
|
||||
|
||||
// Use router scenarios
|
||||
const router = ctx.app.router
|
||||
|
||||
// Get relays for reading a profile
|
||||
const readRelays = router.ForPubkey(pubkey).getUrls()
|
||||
|
||||
// Get relays for publishing
|
||||
const writeRelays = router.FromUser().getUrls()
|
||||
|
||||
// Get relays for a thread
|
||||
const threadRelays = router.Replies(event).getUrls()
|
||||
```
|
||||
|
||||
## Thread Navigation
|
||||
|
||||
```typescript
|
||||
import {ctx} from '@welshman/lib'
|
||||
import {createEvent, NOTE} from '@welshman/util'
|
||||
import {publishThunk} from '@welshman/app'
|
||||
|
||||
const loadThread = async (event: TrustedEvent) => {
|
||||
// Get relays for root event
|
||||
const rootRelays = ctx.app.router
|
||||
.EventRoots(event)
|
||||
.getUrls()
|
||||
|
||||
// Get relays for replies
|
||||
const replyRelays = ctx.app.router
|
||||
.EventParents(event)
|
||||
.getUrls()
|
||||
|
||||
// Get relays for mentions
|
||||
const mentionRelays = ctx.app.router
|
||||
.EventMentions(event)
|
||||
.getUrls()
|
||||
|
||||
// Load from all relevant relays
|
||||
await Promise.all([
|
||||
subscribe({filters, relays: rootRelays}),
|
||||
subscribe({filters, relays: replyRelays}),
|
||||
subscribe({filters, relays: mentionRelays})
|
||||
])
|
||||
}
|
||||
|
||||
// Posting a reply
|
||||
const reply = async (parent: TrustedEvent, content: string) => {
|
||||
const event = createEvent(NOTE, {content})
|
||||
|
||||
// Get optimal relays for publishing
|
||||
const relays = ctx.app.router
|
||||
.PublishEvent(event)
|
||||
// Skip .onion relays
|
||||
.allowOnion(false)
|
||||
// Allow up to 5 relays
|
||||
.limit(5)
|
||||
.getUrls()
|
||||
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
```
|
||||
|
||||
## Router Features
|
||||
|
||||
- Smart relay selection based on context
|
||||
- Quality scoring of relays
|
||||
- Fallback strategies
|
||||
- Handling of special relay types (.onion, local)
|
||||
- Automatic weight calculation
|
||||
- Connection state awareness
|
||||
- NIP-65 compliance
|
||||
|
||||
The router is central to efficient nostr operations, ensuring events reach their intended audience while minimizing unnecessary network traffic.
|
||||
+5
-80
@@ -1,16 +1,16 @@
|
||||
# Session Management
|
||||
|
||||
The session system provides a unified way to handle different authentication methods:
|
||||
- Secret Key NIP-01
|
||||
- Nostr Extensions NIP-07
|
||||
- Bunker URL NIP-46
|
||||
- Amber or in-device NIP-55
|
||||
|
||||
while managing user state and encryption capabilities.
|
||||
- NIP-01 via Secret Key
|
||||
- NIP-07 via Browser Extension
|
||||
- NIP-46 via Bunker URL or Nostrconnect
|
||||
- NIP-55 via Android Signer Application
|
||||
|
||||
## Overview
|
||||
|
||||
Sessions are stored in local storage and can be:
|
||||
|
||||
- Persisted across page reloads
|
||||
- Used with multiple accounts
|
||||
- Switched dynamically
|
||||
@@ -18,39 +18,6 @@ Sessions are stored in local storage and can be:
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import {ctx, setContext} from '@welshman/lib'
|
||||
import {
|
||||
getDefaultNetContext,
|
||||
getDefaultAppContext,
|
||||
pubkey,
|
||||
sessions,
|
||||
session,
|
||||
addSession,
|
||||
getNip07
|
||||
} from '@welshman/app'
|
||||
|
||||
// Set up app config
|
||||
setContext({
|
||||
net: getDefaultNetContext(),
|
||||
app: getDefaultAppContext(),
|
||||
})
|
||||
|
||||
// Log in via NIP-07 extension (browser wallet)
|
||||
if (await getNip07()) {
|
||||
addSession({
|
||||
method: 'nip07',
|
||||
pubkey: await getNip07().getPublicKey()
|
||||
})
|
||||
}
|
||||
|
||||
// Get current session
|
||||
console.log(session.get()) // Current active session
|
||||
console.log(pubkey.get()) // Current pubkey
|
||||
```
|
||||
|
||||
## Multiple Sessions
|
||||
|
||||
```typescript
|
||||
import {sessions, pubkey, addSession, dropSession} from '@welshman/app'
|
||||
|
||||
@@ -118,23 +85,6 @@ const encrypted = await signer.get().nip44.encrypt(
|
||||
)
|
||||
```
|
||||
|
||||
## Session Persistence
|
||||
|
||||
Sessions are automatically persisted to local storage. On page load:
|
||||
|
||||
```typescript
|
||||
import {pubkey, sessions} from '@welshman/app'
|
||||
|
||||
// Sessions load automatically from local storage
|
||||
console.log(sessions.get()) // All stored sessions
|
||||
|
||||
// the current active session
|
||||
console.log(session.get())
|
||||
|
||||
// Last active pubkey is restored
|
||||
console.log(pubkey.get())
|
||||
```
|
||||
|
||||
## Session Types
|
||||
|
||||
```typescript
|
||||
@@ -159,28 +109,3 @@ type SessionNip01 = {
|
||||
secret: string
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import {tryCatch} from '@welshman/lib'
|
||||
import {addSession, getNip07} from '@welshman/app'
|
||||
|
||||
const login = async () => {
|
||||
const nip07 = await tryCatch(getNip07)
|
||||
|
||||
if (!nip07) {
|
||||
throw new Error("No NIP-07 extension found")
|
||||
}
|
||||
|
||||
const pubkey = await tryCatch(
|
||||
() => nip07.getPublicKey()
|
||||
)
|
||||
|
||||
if (!pubkey) {
|
||||
throw new Error("Failed to get public key")
|
||||
}
|
||||
|
||||
addSession({method: 'nip07', pubkey})
|
||||
}
|
||||
```
|
||||
|
||||
+16
-52
@@ -1,67 +1,31 @@
|
||||
# Storage
|
||||
|
||||
The storage system provides IndexedDB persistence for stores and repositories.
|
||||
It's critical to initialize this early in your application lifecycle to ensure data consistency.
|
||||
|
||||
Initialize this early in your application lifecycle to ensure data consistency.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
initStorage,
|
||||
storageAdapters,
|
||||
throttled,
|
||||
repository,
|
||||
tracker,
|
||||
relays,
|
||||
handles,
|
||||
freshness,
|
||||
plaintext
|
||||
} from '@welshman/app'
|
||||
import {initStorage, defaultStorageAdapters} from '@welshman/app'
|
||||
|
||||
// Real world example from Coracle
|
||||
const initializeStorage = async () => {
|
||||
const ready = initStorage("coracle-db", 1, {
|
||||
// Persist relay info
|
||||
relays: {
|
||||
keyPath: "url",
|
||||
store: throttled(3000, relays)
|
||||
// Use default storage adapters, which track important metadata events,
|
||||
// relays, handles, zappers, etc.
|
||||
await initStorage("my-db", 1, {
|
||||
...defaultStorageAdapters,
|
||||
custom: {
|
||||
keyPath: "key",
|
||||
init: async () => console.log(await getAll("custom")),
|
||||
sync: () => {
|
||||
// Set up a listener for changes, using bulkPut to save records.
|
||||
// Return an unsubscribe function for cleanup
|
||||
},
|
||||
|
||||
// Persist NIP-05 handles
|
||||
handles: {
|
||||
keyPath: "nip05",
|
||||
store: throttled(3000, handles)
|
||||
},
|
||||
|
||||
// Track data freshness
|
||||
freshness: storageAdapters.fromObjectStore(
|
||||
freshness,
|
||||
{throttle: 3000}
|
||||
),
|
||||
|
||||
// Store decrypted content
|
||||
plaintext: storageAdapters.fromObjectStore(
|
||||
plaintext,
|
||||
{throttle: 3000}
|
||||
),
|
||||
|
||||
// Store events and their sources
|
||||
events: storageAdapters.fromRepositoryAndTracker(
|
||||
repository,
|
||||
tracker,
|
||||
{throttle: 3000}
|
||||
)
|
||||
})
|
||||
|
||||
// Wait for storage to be ready
|
||||
await ready
|
||||
|
||||
// App can now start loading data
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The storage system:
|
||||
|
||||
- Persists data across page reloads
|
||||
- Throttles writes for performance
|
||||
- Handles store migrations
|
||||
- Syncs bidirectionally
|
||||
- Supports custom adapters
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Stores and Loaders
|
||||
|
||||
The `@welshman/app` package provides a powerful system of collection-based reactive stores and loader utilities.
|
||||
|
||||
These utilities follow a consistent pattern for working with various types of Nostr data, making it easy to:
|
||||
|
||||
1. Query data from the repository
|
||||
2. Transform it into application-specific structures
|
||||
3. Access it reactively in your UI
|
||||
4. Trigger network loading when needed
|
||||
|
||||
## Core Concept
|
||||
|
||||
Each collection-based module exports a similar set of utilities:
|
||||
|
||||
```typescript
|
||||
// Common pattern across collection-based modules
|
||||
export const {
|
||||
// Main collection store (derived from repository)
|
||||
store: follows,
|
||||
|
||||
// Indexed map for efficient lookup
|
||||
indexStore: followsByPubkey,
|
||||
|
||||
// Function to get a reactive store for a specific item
|
||||
deriveItem: deriveFollows,
|
||||
|
||||
// Function to trigger loading an item from the network
|
||||
loadItem: loadFollows
|
||||
} = collection({
|
||||
name: "collection-name",
|
||||
store: baseStore,
|
||||
getKey: item => item.keyProperty,
|
||||
load: async (key) => { /* Loading logic */ }
|
||||
})
|
||||
```
|
||||
|
||||
## Available Collections
|
||||
|
||||
| Collection | Key | Kind | Description |
|
||||
|------------|-----|------|-------------|
|
||||
| `follows` | pubkey | 3 | User follow lists |
|
||||
| `mutes` | pubkey | 10000 | User mute lists |
|
||||
| `pins` | pubkey | 10001 | User pinned items |
|
||||
| `profiles` | pubkey | 0 | User profile metadata |
|
||||
| `relaySelections` | pubkey | 10002 | User relay preferences |
|
||||
| `inboxRelaySelections` | pubkey | 10005 | User inbox relay settings |
|
||||
| `zappers` | lnurl | - | Lightning zapper metadata |
|
||||
| `handles` | nip05 | - | NIP-05 identifier metadata |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Loading and Accessing Data
|
||||
|
||||
```typescript
|
||||
import { loadProfile, deriveProfile, profilesByPubkey } from '@welshman/app'
|
||||
|
||||
// Trigger loading a profile from the network
|
||||
await loadProfile('pubkey123')
|
||||
|
||||
// Get a reactive store for a specific profile
|
||||
const profile = deriveProfile('pubkey123')
|
||||
|
||||
// Access all profiles by pubkey
|
||||
const allProfiles = profilesByPubkey.get()
|
||||
const specificProfile = allProfiles.get('pubkey123')
|
||||
```
|
||||
|
||||
### User-Specific Collections
|
||||
|
||||
Several modules provide user-specific derived stores that automatically load data for the currently signed-in user:
|
||||
|
||||
```typescript
|
||||
import { userProfile, userFollows, userMutes, userPins } from '@welshman/app'
|
||||
|
||||
// These are derived stores that automatically:
|
||||
// 1. Watch for changes to the current user's pubkey
|
||||
// 2. Load the appropriate data when the user changes
|
||||
// 3. Provide the data reactively
|
||||
|
||||
userProfile.subscribe(profile => {
|
||||
// Current user's profile data
|
||||
})
|
||||
|
||||
userFollows.subscribe(follows => {
|
||||
// Current user's follow list
|
||||
})
|
||||
```
|
||||
|
||||
### Web of Trust Utilities
|
||||
|
||||
The `wot.ts` module provides additional utilities for analyzing the social graph:
|
||||
|
||||
```typescript
|
||||
import { getFollows, getFollowers, getNetwork, getWotScore } from '@welshman/app'
|
||||
|
||||
// Get users followed by a pubkey
|
||||
const followedUsers = getFollows('pubkey123')
|
||||
|
||||
// Get users following a pubkey
|
||||
const followers = getFollowers('pubkey123')
|
||||
|
||||
// Get extended network (follows-of-follows)
|
||||
const network = getNetwork('pubkey123')
|
||||
|
||||
// Calculate trust score between users
|
||||
const score = getWotScore('userPubkey', 'targetPubkey')
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# Subscription System
|
||||
|
||||
The subscription system extends Nostr's base subscription model with intelligent caching, repository integration, and configurable behaviors.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Local Repository**: Events are automatically cached and tracked
|
||||
- **Cache Intelligence**: Smart decisions about when to use cached data
|
||||
- **Relay Integration**: Works with the router for optimal relay selection
|
||||
- **Configurable Behavior**: Control caching and timeouts
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```typescript
|
||||
type SubscribeRequest = {
|
||||
// Required
|
||||
filters: Filter[] // What to query
|
||||
|
||||
// Behavior Control
|
||||
closeOnEose?: boolean // Auto-close and use cache
|
||||
timeout?: number // Max time to wait
|
||||
authTimeout?: number // Time for auth negotiation
|
||||
requestDelay?: number // Delay between batched requests
|
||||
|
||||
// Optional
|
||||
relays?: string[] // Specific relays to query
|
||||
|
||||
// Event Handlers
|
||||
onEvent?: (event: TrustedEvent) => void
|
||||
onEose?: (url: string) => void
|
||||
onComplete?: () => void
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Behavior Control
|
||||
|
||||
The `closeOnEose` parameter is crucial for controlling caching behavior:
|
||||
|
||||
```typescript
|
||||
// WITH closeOnEose: true (default for load())
|
||||
// - Checks cache first
|
||||
// - Returns cached results if complete
|
||||
// - Closes after EOSE
|
||||
// - Good for: Known events, historical data
|
||||
const loadKnownEvent = async (id: string) => {
|
||||
const events = await load({
|
||||
filters: [{ids: [id]}],
|
||||
closeOnEose: true
|
||||
})
|
||||
return events[0]
|
||||
}
|
||||
|
||||
// WITH closeOnEose: false
|
||||
// - Always queries relays
|
||||
// - Stays open for updates
|
||||
// - Ignores cache completeness
|
||||
// - Good for: Replaceable events, live data
|
||||
const watchProfile = (pubkey: string) => {
|
||||
return subscribe({
|
||||
filters: [{
|
||||
kinds: [PROFILE],
|
||||
authors: [pubkey]
|
||||
}],
|
||||
closeOnEose: false // Force relay query
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Common Usage Patterns
|
||||
|
||||
### One-time Queries
|
||||
|
||||
```typescript
|
||||
// Load specific event
|
||||
const event = await load({
|
||||
filters: [{ids: [eventId]}]
|
||||
// closeOnEose: true by default
|
||||
})
|
||||
|
||||
// Load latest profile
|
||||
const profile = await load({
|
||||
filters: [{
|
||||
kinds: [PROFILE],
|
||||
authors: [pubkey],
|
||||
limit: 1
|
||||
}],
|
||||
closeOnEose: false // Get latest from network
|
||||
})
|
||||
```
|
||||
|
||||
### Live Subscriptions
|
||||
|
||||
```typescript
|
||||
// Watch for updates
|
||||
const sub = subscribe({
|
||||
filters: [{
|
||||
kinds: [NOTE],
|
||||
since: now() // Only new events
|
||||
}],
|
||||
closeOnEose: false, // Stay open
|
||||
})
|
||||
|
||||
sub.on('event', (url, event) => {
|
||||
// Handle live events
|
||||
})
|
||||
```
|
||||
|
||||
### Smart Caching
|
||||
|
||||
```typescript
|
||||
// Profile loader with refresh control
|
||||
const loadProfile = async (pubkey: string, options = {}) => {
|
||||
const {
|
||||
forceRefresh = false, // Skip cache
|
||||
timeout = 3000, // Max wait time
|
||||
relays = [] // Optional relay override
|
||||
} = options
|
||||
|
||||
// Get optimal relays if not specified
|
||||
const targetRelays = relays.length > 0
|
||||
? relays
|
||||
: ctx.app.router.ForPubkey(pubkey).getUrls()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const sub = subscribe({
|
||||
filters: [{
|
||||
kinds: [PROFILE],
|
||||
authors: [pubkey],
|
||||
limit: 1
|
||||
}],
|
||||
relays: targetRelays,
|
||||
closeOnEose: !forceRefresh, // Control cache behavior
|
||||
timeout,
|
||||
|
||||
onEvent: (url, event) => {
|
||||
resolve(event)
|
||||
sub.close()
|
||||
},
|
||||
|
||||
onComplete: () => resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Integration
|
||||
|
||||
All events from subscriptions are automatically:
|
||||
- Saved to the repository
|
||||
- Tracked to their source relay
|
||||
- Checked against deletion status
|
||||
|
||||
The repository serves as an intelligent cache layer, making subsequent queries for the same data faster.
|
||||
+5
-28
@@ -1,12 +1,13 @@
|
||||
# Tag Utilities
|
||||
|
||||
The tag utilities provide helper functions for creating properly formatted Nostr event tags with correct relay hints and metadata.
|
||||
|
||||
These are especially useful when creating events that reference other events or users.
|
||||
|
||||
## Tag Creators
|
||||
|
||||
### Pubkey Tags
|
||||
|
||||
### User Tags
|
||||
```typescript
|
||||
import {tagPubkey} from '@welshman/app'
|
||||
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
tagEvent, // Basic event reference
|
||||
tagEventForQuote, // For quoting events
|
||||
tagEventForReply, // For reply threads
|
||||
tagEventForComment, // For NIP-23 comments
|
||||
tagEventForComment, // For NIP-22 comments
|
||||
tagEventForReaction // For reactions
|
||||
} from '@welshman/app'
|
||||
|
||||
@@ -36,34 +37,10 @@ const createReply = async (parent: TrustedEvent, content: string) => {
|
||||
// - Relay hints
|
||||
const tags = tagEventForReply(parent)
|
||||
|
||||
const event = await signer.get().sign(
|
||||
createEvent(NOTE, {
|
||||
content,
|
||||
tags,
|
||||
created_at: now()
|
||||
})
|
||||
)
|
||||
|
||||
return publishThunk({
|
||||
event,
|
||||
// Use relay hints from tags
|
||||
relays: ctx.app.router.PublishEvent(event).getUrls()
|
||||
relays: Router.get().PublishEvent(event).getUrls()
|
||||
event: await signer.get().sign(createEvent(NOTE, {content, tags})),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
All tag creators:
|
||||
- Add appropriate relay hints using the router
|
||||
- Handle replaceable/parameterized events
|
||||
- Follow adequate NIP-10/NIP-22 conventions for threading
|
||||
- Include metadata like usernames
|
||||
- Deduplicate references
|
||||
- Preserve tag order
|
||||
|
||||
The tagging system is crucial for:
|
||||
- Thread construction
|
||||
- Event reactions
|
||||
- User mentions
|
||||
- Zap splits
|
||||
|
||||
Tag utilities ensure consistent and correct tag creation across the application while integrating with the router for relay hints.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Topics
|
||||
|
||||
The topics system provides a reactive way to track and count hashtags across all events in the repository. It automatically updates as new events arrive or are removed.
|
||||
|
||||
```typescript
|
||||
import {topics} from '@welshman/app'
|
||||
|
||||
// In a Svelte component
|
||||
<script>
|
||||
// Reactive list of all topics with counts
|
||||
$: topicList = $topics
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 20)
|
||||
</script>
|
||||
|
||||
<div class="topics">
|
||||
{#each topicList as {name, count}}
|
||||
<a href="/t/{name}">
|
||||
#{name}
|
||||
<span class="count">({count})</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
```
|
||||
|
||||
The store:
|
||||
- Updates automatically with new events
|
||||
- Maintains topic counts
|
||||
- Is throttled to prevent excess updates
|
||||
- Is case-insensitive
|
||||
- Integrates with the repository
|
||||
|
||||
Think of it as a live tag cloud that stays in sync with your local event cache.
|
||||
|
||||
This is commonly used for:
|
||||
- Tag clouds
|
||||
- Topic discovery
|
||||
- Content organization
|
||||
- Trending topics
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Web of Trust (WOT) Module
|
||||
# Web of Trust (WOT)
|
||||
|
||||
The `wot.ts` module provides utilities for implementing a Web of Trust system within Nostr applications. This system analyzes social connections (follows and mutes) to build a reputation graph that can be used for content filtering, user scoring, and discovery.
|
||||
Welshman provides utilities for implementing a Web of Trust system within Nostr applications. This system analyzes social connections (follows and mutes) to build a reputation graph that can be used for content filtering, user scoring, and discovery.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @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.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @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
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @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
@@ -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.
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -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
@@ -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;
|
||||
```
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
@@ -1,5 +1,7 @@
|
||||
# @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
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# @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
@@ -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).
|
||||
[](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
@@ -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.
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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
|
||||
``` -->
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# @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}),
|
||||
})
|
||||
```
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
export * from "./adapters.js"
|
||||
export * from "./context.js"
|
||||
export * from "./core.js"
|
||||
export * from "./collection.js"
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# @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()
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
# @welshman/editor [](https://npmjs.com/package/@welshman/editor)
|
||||
|
||||
A batteries-included nostr-editor.
|
||||
@@ -1,34 +0,0 @@
|
||||
# @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)
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
# @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.
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
||||
# @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))),
|
||||
])
|
||||
)
|
||||
},
|
||||
}))
|
||||
```
|
||||
@@ -166,3 +166,5 @@ export class MultiPublish extends EventEmitter {
|
||||
this.removeAllListeners()
|
||||
}
|
||||
}
|
||||
|
||||
export const publish = (options: MultiPublishOptions) => new MultiPublish(options)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# @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))),
|
||||
])
|
||||
)
|
||||
},
|
||||
}))
|
||||
```
|
||||
@@ -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>()
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# @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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# @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
|
||||
Reference in New Issue
Block a user