diff --git a/.ackrc b/.ackrc index b5287f2..4c505ae 100644 --- a/.ackrc +++ b/.ackrc @@ -1,4 +1,3 @@ ---ignore-dir=docs --ignore-dir=docs/reference --ignore-dir=docs/.vitepress/cache --ignore-dir=dist diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 80662f4..88ec8f9 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -109,14 +109,6 @@ export default defineConfig({ {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/", diff --git a/docs/dvm/handler.md b/docs/dvm/handler.md deleted file mode 100644 index a663654..0000000 --- a/docs/dvm/handler.md +++ /dev/null @@ -1,107 +0,0 @@ -# DVM (Data Vending Machine) Handler - -The DVM Handler module provides a framework for creating and managing Data Vending Machines in the Nostr ecosystem. -A DVM is a service that listens for specific kinds of events and responds with processed data. - -## Core Concepts - -### DVM Handler -```typescript -type DVMHandler = { - stop?: () => void - handleEvent: (e: TrustedEvent) => AsyncGenerator -} -``` -A handler defines how to process specific kinds of events and generate responses. - -### DVM Options -```typescript -type DVMOpts = { - sk: string // Private key for signing responses - relays: string[] // Relays to connect to - handlers: Record // Event handlers by kind - expireAfter?: number // Response expiration time in seconds - requireMention?: boolean // Require DVM to be mentioned in event -} -``` - -## Creating a DVM - -```typescript -import { DVM } from '@welshman/dvm' - -// Create handlers for different event kinds -const handlers = { - // Handler for kind 5001 - "5001": (dvm: DVM) => ({ - handleEvent: async function*(event: TrustedEvent) { - // Process event and yield responses - yield { - kind: 6001, - content: "Processed result", - created_at: now(), - tags: [] - } - } - }) -} - -// Initialize DVM -const dvm = new DVM({ - sk: "your-private-key", - relays: ["wss://relay.example.com"], - handlers, - expireAfter: 3600, // 1 hour - requireMention: true -}) - -// Start the DVM -await dvm.start() -``` - - -## Example Implementation - -```typescript -import { DVM, CreateDVMHandler } from '@welshman/dvm' -import { now } from '@welshman/lib' - -// Create a search handler -const createSearchHandler: CreateDVMHandler = (dvm) => ({ - handleEvent: async function*(event) { - const query = event.content - const results = await performSearch(query) - - yield { - kind: 6001, - content: JSON.stringify(results), - created_at: now(), - tags: [ - ["search", query], - ["results", String(results.length)] - ] - } - } -}) - -// Initialize DVM -const searchDVM = new DVM({ - sk: process.env.DVM_KEY!, - relays: ["wss://relay1.com", "wss://relay2.com"], - handlers: { - "5001": createSearchHandler - }, - expireAfter: 24 * 60 * 60, // 24 hours - requireMention: true -}) - -// Start DVM -await searchDVM.start() - -// Stop DVM when needed -process.on('SIGINT', () => { - searchDVM.stop() -}) -``` - -The DVM Handler provides a robust foundation for building Nostr data services, with built-in support for common requirements like deduplication, response signing, and metadata management. diff --git a/docs/dvm/index.md b/docs/dvm/index.md deleted file mode 100644 index 42cf60b..0000000 --- a/docs/dvm/index.md +++ /dev/null @@ -1,104 +0,0 @@ -# @welshman/dvm - -[![version](https://badgen.net/npm/v/@welshman/dvm)](https://npmjs.com/package/@welshman/dvm) - -`@welshman/dvm` is a comprehensive package for building and interacting with Data Vending Machines (DVMs) in the Nostr ecosystem. It provides both server-side DVM implementation capabilities and client-side request handling. - -## What is a DVM? - -A Data Vending Machine (DVM) is a Nostr service that: -- Listens for specific kinds of events -- Processes these events according to defined rules -- 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 - -```bash -npm install @welshman/dvm -``` diff --git a/docs/dvm/request.md b/docs/dvm/request.md deleted file mode 100644 index 73b9e94..0000000 --- a/docs/dvm/request.md +++ /dev/null @@ -1,119 +0,0 @@ -# DVM Request - -The DVM Request module provides utilities for making requests to Data Vending Machines (DVMs) and handling their responses. -It includes support for progress tracking and result handling. - -## Core Types - -### DVMRequestOptions -```typescript -type DVMRequestOptions = { - event: SignedEvent // The event to send to the DVM - relays: string[] // Relays to use - timeout?: number // Request timeout in milliseconds - autoClose?: boolean // Auto-close subscription after result - reportProgress?: boolean // Listen for progress events -} -``` - -### DVMEvent Enum -```typescript -enum DVMEvent { - Progress = "progress", // DVM progress updates (kind 7000) - Result = "result" // Final DVM result -} -``` - -## Making DVM Requests - -### Basic Usage -```typescript -import { makeDvmRequest, DVMEvent } from '@welshman/dvm' - -const request = makeDvmRequest({ - event: signedEvent, - relays: ["wss://relay.example.com"], - timeout: 30000, // 30 seconds -}) - -// Handle results -request.emitter.on(DVMEvent.Result, (url, event) => { - console.log('Received result:', event) -}) - -// Handle progress updates -request.emitter.on(DVMEvent.Progress, (url, event) => { - console.log('Progress update:', event) -}) -``` - -## Response Handling - -### Result Events -```typescript -request.emitter.on(DVMEvent.Result, (url: string, event: TrustedEvent) => { - // Handle the DVM result - const result = JSON.parse(event.content) - - // Process tags - const requestTag = event.tags.find(t => t[0] === 'request') - const expirationTag = event.tags.find(t => t[0] === 'expiration') -}) -``` - -### Progress Updates -```typescript -request.emitter.on(DVMEvent.Progress, (url: string, event: TrustedEvent) => { - // Handle progress update (kind 7000) - const progress = JSON.parse(event.content) - console.log(`Progress: ${progress.percentage}%`) -}) -``` - -## Complete Example - -```typescript -import { makeDvmRequest, DVMEvent } from '@welshman/dvm' -import { createEvent, finalizeEvent } from '@welshman/util' - -async function queryDVM() { - // Create the request event - const event = createEvent(5001, { - content: JSON.stringify({ - query: "search terms" - }) - }) - - // Sign the event - const signedEvent = finalizeEvent(event, privateKey) - - // Make the request - const dvmRequest = makeDvmRequest({ - event: signedEvent, - relays: ["wss://relay.example.com"], - timeout: 30000, - reportProgress: true - }) - - // Handle progress updates - dvmRequest.emitter.on(DVMEvent.Progress, (url, event) => { - console.log('Progress:', event.content) - }) - - // Return a promise that resolves with the result - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - dvmRequest.sub.close() - reject(new Error('DVM request timeout')) - }, 30000) - - dvmRequest.emitter.on(DVMEvent.Result, (url, event) => { - clearTimeout(timeout) - resolve(event) - }) - }) -} -``` - - -This module simplifies the process of making requests to DVMs while providing flexibility in handling responses and progress updates. diff --git a/docs/index.md b/docs/index.md index 391175d..790ee77 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,9 +39,6 @@ features: - title: "@welshman/content" details: Parser and renderer for nostr notes with customizable formatting options. link: "/content" - - title: "@welshman/dvm" - details: Tools for building and interacting with nostr Data Vending Machines (DVMs) - link: "/dvm" - title: "@welshman/editor" details: Rich text editor with support for mentions and embeds. link: "/editor" diff --git a/packages/app/package.json b/packages/app/package.json index 2ac2e17..ee9e5ea 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,7 +21,6 @@ }, "dependencies": { "@types/throttle-debounce": "^5.0.2", - "@welshman/dvm": "workspace:*", "@welshman/feeds": "workspace:*", "@welshman/lib": "workspace:*", "@welshman/relay": "workspace:*", diff --git a/packages/app/tsconfig.build.json b/packages/app/tsconfig.build.json index 1970520..3b1534b 100644 --- a/packages/app/tsconfig.build.json +++ b/packages/app/tsconfig.build.json @@ -4,7 +4,6 @@ "compilerOptions": { "outDir": "./dist", "paths": { - "@welshman/dvm": ["../dvm/src/index.js"], "@welshman/feeds": ["../feeds/src/index.js"], "@welshman/lib": ["../lib/src/index.js"], "@welshman/relay": ["../relay/src/index.js"], diff --git a/packages/dvm/.eslintignore b/packages/dvm/.eslintignore deleted file mode 100644 index 4c72c12..0000000 --- a/packages/dvm/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -build -normalize-url -__tests__ \ No newline at end of file diff --git a/packages/dvm/package.json b/packages/dvm/package.json deleted file mode 100644 index 6ab5b8a..0000000 --- a/packages/dvm/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@welshman/dvm", - "version": "0.3.4", - "author": "hodlbod", - "license": "MIT", - "description": "A collection of utilities for building nostr DVMs.", - "publishConfig": { - "access": "public" - }, - "type": "module", - "main": "dist/dvm/src/index.js", - "types": "dist/dvm/src/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "pnpm run clean && pnpm run compile --force", - "clean": "rimraf ./dist", - "compile": "tsc -b tsconfig.build.json", - "prepublishOnly": "pnpm run build" - }, - "dependencies": { - "@noble/hashes": "^1.6.1", - "@welshman/lib": "workspace:*", - "@welshman/net": "workspace:*", - "@welshman/util": "workspace:*", - "@welshman/signer": "workspace:*" - }, - "devDependencies": { - "rimraf": "~6.0.0", - "typescript": "~5.8.0" - } -} diff --git a/packages/dvm/src/handler.ts b/packages/dvm/src/handler.ts deleted file mode 100644 index 33f9155..0000000 --- a/packages/dvm/src/handler.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {now} from "@welshman/lib" -import {Nip01Signer} from "@welshman/signer" -import {TrustedEvent, StampedEvent, Filter} from "@welshman/util" -import {request, publish, AdapterContext} from "@welshman/net" - -export type DVMHandler = { - stop?: () => void - handleEvent: (e: TrustedEvent) => AsyncGenerator -} - -export type CreateDVMHandler = (dvm: DVM) => DVMHandler - -export type DVMOpts = { - sk: string - relays: string[] - handlers: Record - expireAfter?: number - requireMention?: boolean - context?: AdapterContext -} - -export class DVM { - active = false - logEvents = false - seen = new Set() - handlers = new Map() - signer: Nip01Signer - - constructor(readonly opts: DVMOpts) { - this.signer = new Nip01Signer(opts.sk) - - for (const [kind, createHandler] of Object.entries(this.opts.handlers)) { - this.handlers.set(parseInt(kind), createHandler(this)) - } - } - - async start() { - this.active = true - - const {relays, context, requireMention = false} = this.opts - const pubkey = await this.signer.getPubkey() - - while (this.active) { - await new Promise(resolve => { - const since = now() - const kinds = Array.from(this.handlers.keys()) - const filter: Filter = {kinds, since} - - if (requireMention) { - filter["#p"] = [pubkey] - } - - request({ - relays, - filters: [filter], - context, - onClose: resolve, - onEvent: this.onEvent, - }) - }) - } - } - - stop() { - for (const handler of this.handlers.values()) { - handler.stop?.() - } - - this.active = false - } - - async onEvent(request: TrustedEvent) { - const {expireAfter = 60 * 60} = this.opts - - if (this.seen.has(request.id)) { - return - } - - const handler = this.handlers.get(request.kind) - - if (!handler) { - return - } - - this.seen.add(request.id) - - if (this.logEvents) { - console.info("Handling request", request) - } - - for await (const event of handler.handleEvent(request)) { - if (event.kind !== 7000) { - event.tags.push(["request", JSON.stringify(request)]) - - const inputTag = request.tags.find((t: string[]) => t[0] === "i") - - if (inputTag) { - event.tags.push(inputTag) - } - } - - event.tags.push(["p", request.pubkey]) - event.tags.push(["e", request.id]) - - if (expireAfter) { - event.tags.push(["expiration", String(now() + expireAfter)]) - } - - if (this.logEvents) { - console.info("Publishing event", event) - } - - this.publish(event) - } - } - - async publish(template: StampedEvent) { - const {relays, context} = this.opts - const event = await this.signer.sign(template) - - await publish({event, relays, context}) - } -} diff --git a/packages/dvm/src/index.ts b/packages/dvm/src/index.ts deleted file mode 100644 index ea75dd2..0000000 --- a/packages/dvm/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./handler.js" -export * from "./request.js" diff --git a/packages/dvm/src/request.ts b/packages/dvm/src/request.ts deleted file mode 100644 index ab05464..0000000 --- a/packages/dvm/src/request.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {now} from "@welshman/lib" -import {TrustedEvent, SignedEvent, Filter} from "@welshman/util" -import {request, publish, AdapterContext} from "@welshman/net" - -export type DVMRequestOptions = { - event: SignedEvent - relays: string[] - timeout?: number - autoClose?: boolean - context?: AdapterContext - onResult?: (event: TrustedEvent, url: string) => void - onProgress?: (event: TrustedEvent, url: string) => void -} - -export const requestDvmResponse = (options: DVMRequestOptions) => { - const {event, relays, context, timeout = 30_000, autoClose = true, onResult, onProgress} = options - const kind = event.kind + 1000 - const kinds = onProgress ? [kind, 7000] : [kind] - const filters: Filter[] = [{kinds, since: now() - 60, "#e": [event.id]}] - const abortController = new AbortController() - const signal = AbortSignal.any([abortController.signal, AbortSignal.timeout(timeout)]) - - return request({ - signal, - relays, - filters, - context, - onEvent: (event: TrustedEvent, url: string) => { - if (event.kind === 7000) { - onProgress?.(event, url) - } else { - onResult?.(event, url) - - if (autoClose) { - abortController.abort() - } - } - }, - }) -} - -export const makeDvmRequest = (options: DVMRequestOptions) => - Promise.all([publish(options), requestDvmResponse(options)]) diff --git a/packages/dvm/tsconfig.build.json b/packages/dvm/tsconfig.build.json deleted file mode 100644 index 0b9d611..0000000 --- a/packages/dvm/tsconfig.build.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - - "compilerOptions": { - "outDir": "./dist", - "paths": { - "@welshman/lib": ["../lib/src/index.js"], - "@welshman/signer": ["../signer/src/index.js"], - "@welshman/util": ["../util/src/index.js"], - "@welshman/relay": ["../relay/src/index.js"], - "@welshman/net": ["../net/src/index.js"] - } - }, - - "include": [ - "src/**/*" - ] -} diff --git a/packages/dvm/tsconfig.json b/packages/dvm/tsconfig.json deleted file mode 100644 index 4082f16..0000000 --- a/packages/dvm/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} diff --git a/packages/dvm/typedoc.json b/packages/dvm/typedoc.json deleted file mode 100644 index 35fed2c..0000000 --- a/packages/dvm/typedoc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "entryPoints": ["src/index.ts"] -} diff --git a/packages/feeds/package.json b/packages/feeds/package.json index 570d8b7..d99e7d6 100644 --- a/packages/feeds/package.json +++ b/packages/feeds/package.json @@ -21,7 +21,6 @@ }, "dependencies": { "@welshman/lib": "workspace:*", - "@welshman/dvm": "workspace:*", "@welshman/net": "workspace:*", "@welshman/relay": "workspace:*", "@welshman/router": "workspace:*", diff --git a/packages/feeds/src/request.ts b/packages/feeds/src/request.ts index e0a519d..942a42f 100644 --- a/packages/feeds/src/request.ts +++ b/packages/feeds/src/request.ts @@ -12,8 +12,7 @@ import { import {Nip01Signer, ISigner} from "@welshman/signer" import {LOCAL_RELAY_URL} from "@welshman/relay" import {Router, getFilterSelections, addMinimalFallbacks} from "@welshman/router" -import {Tracker, AdapterContext, request} from "@welshman/net" -import {makeDvmRequest} from "@welshman/dvm" +import {Tracker, AdapterContext, request, publish} from "@welshman/net" export type RequestPageOptions = { filters: Filter[] @@ -136,6 +135,10 @@ export const requestDVM = async ({ } const event = await signer.sign(makeEvent(kind, {tags})) + const filters = [{kinds: [event.kind + 1000], since: now() - 60, "#e": [event.id]}] - await makeDvmRequest({relays, event, context, onResult}) + return Promise.all([ + publish({event, relays, context}), + request({filters, relays, context, autoClose: true, onEvent: onResult}), + ]) } diff --git a/vitest.config.ts b/vitest.config.ts index 3405bf6..e61bd95 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,7 +16,6 @@ export default defineConfig({ alias: { "@welshman/app": resolve(__dirname, "packages/app/src"), "@welshman/content": resolve(__dirname, "packages/content/src"), - "@welshman/dvm": resolve(__dirname, "packages/dvm/src"), "@welshman/feeds": resolve(__dirname, "packages/feeds/src"), "@welshman/lib": resolve(__dirname, "packages/lib/src"), "@welshman/net": resolve(__dirname, "packages/net/src"),