diff --git a/.gitignore b/.gitignore
index 589ae648..bad9a145 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.turbo
**/node_modules
*.wasm
**/dist
@@ -19,4 +20,5 @@ packages/kinds
.ansible
.envs
.configs
-.docker
\ No newline at end of file
+.docker
+.svelte-kit
diff --git a/apps/nocapd/package.json b/apps/nocapd/package.json
index 9f266481..f4667528 100644
--- a/apps/nocapd/package.json
+++ b/apps/nocapd/package.json
@@ -10,10 +10,11 @@
"@nostrwatch/controlflow": "^0.1.0",
"@nostrwatch/logger": "^0.0.4",
"@nostrwatch/nocap": "^0.4.2",
+ "@nostrwatch/nocap-every-adapter-default": "1.2.2",
"@nostrwatch/nwcache": "^0.1.2",
- "@nostrwatch/publisher": "^0.4.1",
+ "@nostrwatch/publisher": "^0.4.3",
"@nostrwatch/seed": "^0.0.2",
- "@nostrwatch/utils": "^0.0.3",
+ "@nostrwatch/utils": "0.1.2",
"bluebird": "3.7.2",
"chalk": "5.3.0",
"ngeohash": "^0.6.3",
diff --git a/apps/nocapd/src/classes/Worker.js b/apps/nocapd/src/classes/Worker.js
index f86acdcd..c69d7cea 100644
--- a/apps/nocapd/src/classes/Worker.js
+++ b/apps/nocapd/src/classes/Worker.js
@@ -4,10 +4,14 @@ import chalk from 'chalk';
import { RetryManager } from '@nostrwatch/controlflow'
import Logger from '@nostrwatch/logger'
-import { Nocap } from '@nostrwatch/nocap'
+
import { parseRelayNetwork, delay, lastCheckedId } from '@nostrwatch/utils'
import Publish from '@nostrwatch/publisher'
+import { Nocap } from "@nostrwatch/nocap"
+import nocapAdapters from "@nostrwatch/nocap-every-adapter-default"
+
+
export class NWWorker {
constructor(pubkey, $q, rcache, config){
@@ -91,11 +95,13 @@ export class NWWorker {
async work(job){
this.log.debug(`${this.id()}: work(): ${job.id} checking ${job.data?.relay} for ${this.opts?.checks?.enabled || "unknown checks"}`)
- const failure = (err) => { this.log.debug(`Could not run ${this.pubkey} check for ${job.data.relay}: ${err.message}`) }
+ const failure = (err) => { this.log.err(`Could not run ${this.pubkey} check for ${job.data.relay}: ${err.message}`) }
try {
const { relay:url } = job.data
const nocap = new Nocap(url, this.nocapOpts)
+ await nocap.useAdapters(Object.values(nocapAdapters))
const result = await nocap.check(this.opts.checks.enabled).catch(failure)
+ // console.log(url, result)
return { result }
}
catch(err) {
@@ -105,13 +111,16 @@ export class NWWorker {
}
async on_error(job, err){
+ if(this.hard_stop) return
this.log.debug(`on_error(): ${job.id}: ${err}`)
- await this.on_fail( result )
+ await this.on_fail( job )
}
async on_completed(job, rvalue){
+ if(this.hard_stop) return
this.log.debug(`on_completed(): ${job.id}: ${JSON.stringify(rvalue)}`)
const { result } = rvalue
+ if(!result?.url) return console.error(`url was empty:`, job.id)
let fail = result?.open?.data? false: true
this.progressMessage(result.url, result, fail)
if(fail)
@@ -122,6 +131,7 @@ export class NWWorker {
}
async on_success(result){
+ if(this.hard_stop) return
this.log.debug(`on_success(): ${result.url}`)
if(this.config?.publisher?.kinds?.includes(30066) ){
const publish30066 = new Publish.Kind30066()
@@ -134,10 +144,12 @@ export class NWWorker {
}
async on_fail(result){
+ if(this.hard_stop) return
this.log.debug(`on_fail(): ${result.url}`)
}
async after_completed(result, error=false){
+ if(this.hard_stop) return
this.log.debug(`after_completed(): ${result.url}`)
await this.updateRelayCache( { ...result } )
await this.retry.setRetries( result.url, !error )
@@ -145,6 +157,7 @@ export class NWWorker {
}
cbcall(...args){
+ if(this.hard_stop) return
this.log.debug(`cbcall(): ${JSON.stringify(args)}`)
const handler = [].shift.call(args)
if(this?.[`on_${handler}`] && typeof this[`on_${handler}`] === 'function')
@@ -460,6 +473,6 @@ const evaluateMaxRelays = (evaluate, relays) => {
return parseInt( eval( evaluate ) )
}
catch(e){
- this.log.error(`Error evaluating this.opts.checks.options.max -> "${this?.opts?.checks?.options?.max} || "is undefined"": ${e?.message || "error undefined"}`)
+ this.log.err(`Error evaluating this.opts.checks.options.max -> "${this?.opts?.checks?.options?.max} || "is undefined"": ${e?.message || "error undefined"}`)
}
}
\ No newline at end of file
diff --git a/apps/nocapd/src/daemon.js b/apps/nocapd/src/daemon.js
index d47e12f5..f25e3413 100644
--- a/apps/nocapd/src/daemon.js
+++ b/apps/nocapd/src/daemon.js
@@ -1,3 +1,5 @@
+import "websocket-polyfill";
+
import schedule from 'node-schedule'
import Deferred from 'promise-deferred'
@@ -73,12 +75,31 @@ const stop = async(signal) => {
log.info(`Received ${signal}`);
log.info(`Gracefully shutting down...`)
$q.worker.hard_stop = true
- if(signal !== 'EAI_AGAIN'){
- log.debug(`shutdown progress: $q.worker.pause()`)
- await $q.worker.pause()
- log.debug(`shutdown progress: $q.queue.drain()`)
- await $q.queue.drain()
+ log.info(`shutdown progress: schedule.gracefulShutdown()`)
+ schedule.gracefulShutdown()
+ log.info(`shutdown progress: $q.worker.pause()`)
+ $q.worker.pause()
+ log.info(`shutdown progress: $q.queue.pause()`)
+ $q.queue.pause()
+ log.info(`shutdown progress: $q.queue.drain()`)
+ await $q.queue.drain()
+ log.info(`shutdown progress: checking active jobs`)
+ const {active:numActive} = await $q.queue.getJobCounts('active')
+ if(numActive > 0) {
+ log.info(`shutdown progress: ${numActive} active jobs`)
+ await new Promise( resolve => {
+ $q.queue.on('drained', resolve)
+ })
+ log.info(`shutdown progress: no more jobs`)
}
+ log.info(`shutdown progress: $q.queue.obliterate()`)
+ await $q.queue.obliterate()
+ // if(signal !== 'EAI_AGAIN'){
+
+ // }
+ // else {
+
+ // }
log.debug(`shutdown progress: await rcache.$.close()`)
await rcache.$.close()
log.debug(`shutdown progress: complete!`)
@@ -124,7 +145,7 @@ const schedulePopulator = () =>{
}
const scheduleSyncRelays = () =>{
- const name = "syncRelaysIn()"
+ const name = "scheduleSyncRelays()"
if(!config?.nocapd?.seed?.options?.events) return
const intervalMs = config.nocapd.seed.options.events.interval
log.info(`syncRelaysIn(): scheduling to fire every ${timestring(intervalMs, "s")} seconds`)
@@ -190,14 +211,12 @@ async function gracefulShutdown(signal) {
}
export const Nocapd = async () => {
-
-
config = await loadConfig().catch( (err) => { log.err(err); process.exit() } )
await delay(2000)
rcache = relaycache(process.env.NWCACHE_PATH || './.lmdb')
await migrate(rcache)
await delay(1000)
- await maybeAnnounce()
+ // await maybeAnnounce()
await maybeBootstrap()
$q = await initWorker()
globalHandlers()
diff --git a/demos/kit-with-idb/README.md b/demos/kit-with-idb/README.md
new file mode 100644
index 00000000..7834ad93
--- /dev/null
+++ b/demos/kit-with-idb/README.md
@@ -0,0 +1,41 @@
+# @nostrwatch/demo-kit-with-idb
+
+This is a demo of `@nostrwatch/kit` with the `@nostrwatch/kit-adapter-idb` adapter.
+
+## Usage
+
+
+pnpm
+
+```
+pnpm run install
+pnpm run dev
+```
+
+
+
+
+
+yarn
+
+```
+yarn install
+yarn dev
+```
+
+
+
+
+
+
+npm
+
+```
+npm run install
+npm run dev
+```
+
+
+
+
+
diff --git a/demos/kit-with-idb/package.json b/demos/kit-with-idb/package.json
new file mode 100644
index 00000000..fa090ace
--- /dev/null
+++ b/demos/kit-with-idb/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@nostrwatch/demo-kit-with-idb",
+ "version": "0.0.1",
+ "main": "src/index.ts",
+ "license": "MIT",
+ "type": "module",
+ "dependencies": {
+ "@nostr-dev-kit/ndk": "2.7.1",
+ "@nostr-dev-kit/ndk-cache-dexie": "2.3.1",
+ "@nostr-dev-kit/ndk-svelte": "2.2.15",
+ "@nostrwatch/idb": "0.0.1",
+ "@nostrwatch/kit": "0.0.1",
+ "@nostrwatch/kit-adapter-idb": "0.0.1",
+ "@square/svelte-store": "1.0.18",
+ "@svelte-plugins/tooltips": "3.0.1",
+ "fastq": "1.17.1",
+ "hash-object": "5.0.1",
+ "svelte-bricks": "0.2.1",
+ "svelte-material-ui": "7.0.0",
+ "svelte-time": "0.9.0",
+ "tseep": "1.2.2",
+ "vite-plugin-node-polyfills": "0.22.0"
+ },
+ "devDependencies": {
+ "@smui/button": "7.0.0",
+ "@smui/tooltip": "7.0.0",
+ "@sveltejs/adapter-auto": "^3.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/vite-plugin-svelte": "^3.0.0",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.32",
+ "postcss-load-config": "^5.0.2",
+ "rimraf": "5.0.8",
+ "svelte": "^4.2.7",
+ "svelte-check": "^3.6.0",
+ "tailwindcss": "^3.3.6",
+ "tslib": "^2.4.1",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.3",
+ "vite-plugin-cross-origin-isolation": "0.1.6"
+ },
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "clean": "rimraf dist"
+ }
+}
diff --git a/demos/kit-with-idb/pnpm-lock.yaml b/demos/kit-with-idb/pnpm-lock.yaml
new file mode 100644
index 00000000..b1191faa
--- /dev/null
+++ b/demos/kit-with-idb/pnpm-lock.yaml
@@ -0,0 +1,2160 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ '@nostr-dev-kit/ndk':
+ specifier: 2.7.1
+ version: 2.7.1(typescript@5.4.5)
+ '@nostr-dev-kit/ndk-cache-dexie':
+ specifier: 2.3.1
+ version: 2.3.1(typescript@5.4.5)
+ '@nostr-fetch/adapter-ndk':
+ specifier: 0.15.1
+ version: 0.15.1(@nostr-dev-kit/ndk@2.7.1)(nostr-fetch@0.15.1)
+ dexie:
+ specifier: 4.0.4
+ version: 4.0.4
+ nostr-fetch:
+ specifier: 0.15.1
+ version: 0.15.1
+
+devDependencies:
+ '@sveltejs/adapter-auto':
+ specifier: ^3.0.0
+ version: 3.2.0(@sveltejs/kit@2.5.7)
+ '@sveltejs/kit':
+ specifier: ^2.0.0
+ version: 2.5.7(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.15)(vite@5.2.10)
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^3.0.0
+ version: 3.1.0(svelte@4.2.15)(vite@5.2.10)
+ autoprefixer:
+ specifier: ^10.4.16
+ version: 10.4.19(postcss@8.4.38)
+ postcss:
+ specifier: ^8.4.32
+ version: 8.4.38
+ postcss-load-config:
+ specifier: ^5.0.2
+ version: 5.0.3(postcss@8.4.38)
+ svelte:
+ specifier: ^4.2.7
+ version: 4.2.15
+ svelte-check:
+ specifier: ^3.6.0
+ version: 3.6.9(postcss-load-config@5.0.3)(postcss@8.4.38)(svelte@4.2.15)
+ tailwindcss:
+ specifier: ^3.3.6
+ version: 3.4.3
+ tslib:
+ specifier: ^2.4.1
+ version: 2.6.2
+ typescript:
+ specifier: ^5.0.0
+ version: 5.4.5
+ vite:
+ specifier: ^5.0.3
+ version: 5.2.10
+
+packages:
+
+ /@alloc/quick-lru@5.2.0:
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /@ampproject/remapping@2.3.0:
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+ dev: true
+
+ /@esbuild/aix-ppc64@0.20.2:
+ resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm64@0.20.2:
+ resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm@0.20.2:
+ resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-x64@0.20.2:
+ resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-arm64@0.20.2:
+ resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-x64@0.20.2:
+ resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.20.2:
+ resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-x64@0.20.2:
+ resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm64@0.20.2:
+ resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm@0.20.2:
+ resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ia32@0.20.2:
+ resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-loong64@0.20.2:
+ resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-mips64el@0.20.2:
+ resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ppc64@0.20.2:
+ resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-riscv64@0.20.2:
+ resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-s390x@0.20.2:
+ resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-x64@0.20.2:
+ resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/netbsd-x64@0.20.2:
+ resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/openbsd-x64@0.20.2:
+ resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/sunos-x64@0.20.2:
+ resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-arm64@0.20.2:
+ resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-ia32@0.20.2:
+ resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-x64@0.20.2:
+ resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: true
+
+ /@jridgewell/gen-mapping@0.3.5:
+ resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/trace-mapping': 0.3.25
+ dev: true
+
+ /@jridgewell/resolve-uri@3.1.2:
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/set-array@1.2.1:
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/sourcemap-codec@1.4.15:
+ resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+ dev: true
+
+ /@jridgewell/trace-mapping@0.3.25:
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.4.15
+ dev: true
+
+ /@noble/ciphers@0.2.0:
+ resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
+ dev: false
+
+ /@noble/curves@1.1.0:
+ resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
+ dependencies:
+ '@noble/hashes': 1.3.1
+ dev: false
+
+ /@noble/curves@1.4.0:
+ resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==}
+ dependencies:
+ '@noble/hashes': 1.4.0
+ dev: false
+
+ /@noble/hashes@1.3.1:
+ resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
+ engines: {node: '>= 16'}
+ dev: false
+
+ /@noble/hashes@1.4.0:
+ resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
+ engines: {node: '>= 16'}
+ dev: false
+
+ /@noble/secp256k1@2.1.0:
+ resolution: {integrity: sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==}
+ dev: false
+
+ /@nodelib/fs.scandir@2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat@2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk@1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.17.1
+ dev: true
+
+ /@nostr-dev-kit/ndk-cache-dexie@2.3.1(typescript@5.4.5):
+ resolution: {integrity: sha512-6R0PYLBW8TmX4mHfPxdFXVpypouZ0RFdSKBtITXsiujww0l4MBR3GGOmc8hVpr0O56Qvtr9Kv4P+34U/7PGc7g==}
+ dependencies:
+ '@nostr-dev-kit/ndk': 2.7.1(typescript@5.4.5)
+ debug: 4.3.4
+ dexie: 3.2.7
+ nostr-tools: 1.17.0(typescript@5.4.5)
+ typescript-lru-cache: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ dev: false
+
+ /@nostr-dev-kit/ndk@2.7.1(typescript@5.4.5):
+ resolution: {integrity: sha512-EVN1jy6SXMm0cRvmzZFn2VTTPxcjI515KjBawNtmuQrPwyN+4J7zWnFlWq+KtavWbAuoQnh1rv+b6yWbBhpy5g==}
+ dependencies:
+ '@noble/curves': 1.4.0
+ '@noble/hashes': 1.4.0
+ '@noble/secp256k1': 2.1.0
+ '@scure/base': 1.1.6
+ debug: 4.3.4
+ light-bolt11-decoder: 3.1.1
+ node-fetch: 3.3.2
+ nostr-tools: 1.17.0(typescript@5.4.5)
+ tseep: 1.2.1
+ typescript-lru-cache: 2.0.0
+ utf8-buffer: 1.0.0
+ websocket-polyfill: 0.0.3
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ dev: false
+
+ /@nostr-fetch/adapter-ndk@0.15.1(@nostr-dev-kit/ndk@2.7.1)(nostr-fetch@0.15.1):
+ resolution: {integrity: sha512-srbJGP6jO00IpCNJxuIiD9SfXbRkP1yS2fNjLqobfiWoLaOnz/bjTil9V/jdDDivtVrw4X6OzYgO+TPVzYovpQ==}
+ peerDependencies:
+ '@nostr-dev-kit/ndk': ^1.0.0
+ nostr-fetch: ^0.15.1
+ dependencies:
+ '@nostr-dev-kit/ndk': 2.7.1(typescript@5.4.5)
+ '@nostr-fetch/kernel': 0.15.1
+ nostr-fetch: 0.15.1
+ dev: false
+
+ /@nostr-fetch/kernel@0.15.1:
+ resolution: {integrity: sha512-+KX+n3v9ha2ki5BGL12isw+BrlK36aANkDD98SQMzt+5VFRCnfkFiJSkx+kjAKm0BXdA3YCTRhrqVOYEDuvohQ==}
+ dependencies:
+ '@noble/curves': 1.4.0
+ '@noble/hashes': 1.4.0
+ dev: false
+
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@polka/url@1.0.0-next.25:
+ resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
+ dev: true
+
+ /@rollup/rollup-android-arm-eabi@4.16.1:
+ resolution: {integrity: sha512-92/y0TqNLRYOTXpm6Z7mnpvKAG9P7qmK7yJeRJSdzElNCUnsgbpAsGqerUboYRIQKzgfq4pWu9xVkgpWLfmNsw==}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-android-arm64@4.16.1:
+ resolution: {integrity: sha512-ttWB6ZCfRLuDIUiE0yiu5gcqOsYjA5F7kEV1ggHMj20FwLZ8A1FMeahZJFl/pnOmcnD2QL0z4AcDuo27utGU8A==}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-darwin-arm64@4.16.1:
+ resolution: {integrity: sha512-QLDvPLetbqjHojTGFw9+nuSP3YY/iz2k1cep6crYlr97sS+ZJ0W43b8Z0zC00+lnFZj6JSNxiA4DjboNQMuh1A==}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-darwin-x64@4.16.1:
+ resolution: {integrity: sha512-TAUK/D8khRrRIa1KwRzo8JNKk3tcqaeXWdtsiLgA8zmACWwlWLjPCJ4DULGHQrMkeBjp1Cd3Yuwx04lZgFx5Vg==}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm-gnueabihf@4.16.1:
+ resolution: {integrity: sha512-KO+WGZjrh6zyFTD1alIFkfdtxf8B4BC+hqd3kBZHscPLvE5FR/6QKsyuCT0JlERxxYBSUKNUQ/UHyX5uwO1x2A==}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm-musleabihf@4.16.1:
+ resolution: {integrity: sha512-NqxbllzIB1WoAo4ThUXVtd21iiM5IHMTTXmXySKBLVcZvkU0HIZmatlP7hLzb5yQubcmdIeWmncd2NdsjocEiw==}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm64-gnu@4.16.1:
+ resolution: {integrity: sha512-snma5NvV8y7IECQ5rq0sr0f3UUu+92NVmG/913JXJMcXo84h9ak9TA5UI9Cl2XRM9j3m37QwDBtEYnJzRkSmxA==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm64-musl@4.16.1:
+ resolution: {integrity: sha512-KOvqGprlD84ueivhCi2flvcUwDRD20mAsE3vxQNVEI2Di9tnPGAfEu6UcrSPZbM+jG2w1oSr43hrPo0RNg6GGg==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-powerpc64le-gnu@4.16.1:
+ resolution: {integrity: sha512-/gsNwtiGLqYwN4vP+EIdUC6Q6LTlpupWqokqIndvZcjn9ig/5P01WyaYCU2wvfL/2Z82jp5kX8c1mDBOvCP3zg==}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-riscv64-gnu@4.16.1:
+ resolution: {integrity: sha512-uU8zuGkQfGqfD9w6VRJZI4IuG4JIfNxxJgEmLMAmPVHREKGsxFVfgHy5c6CexQF2vOfgjB33OsET3Vdn2lln9A==}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-s390x-gnu@4.16.1:
+ resolution: {integrity: sha512-lsjLtDgtcGFEuBP6yrXwkRN5/wKlvUZtfbKZZu0yaoNpiBL4epgnO21osAALIspVRnl4qZgyLFd8xjCYYWgwfw==}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-x64-gnu@4.16.1:
+ resolution: {integrity: sha512-N2ZizKhUryqqrMfdCnjhJhZRgv61C6gK+hwVtCIKC8ts8J+go+vqENnGexwg21nHIOvLN5mBM8a7DI2vlyIOPg==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-x64-musl@4.16.1:
+ resolution: {integrity: sha512-5ICeMxqg66FrOA2AbnBQ2TJVxfvZsKLxmof0ibvPLaYtbsJqnTUtJOofgWb46Gjd4uZcA4rdsp4JCxegzQPqCg==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-arm64-msvc@4.16.1:
+ resolution: {integrity: sha512-1vIP6Ce02L+qWD7uZYRiFiuAJo3m9kARatWmFSnss0gZnVj2Id7OPUU9gm49JPGasgcR3xMqiH3fqBJ8t00yVg==}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-ia32-msvc@4.16.1:
+ resolution: {integrity: sha512-Y3M92DcVsT6LoP+wrKpoUWPaazaP1fzbNkp0a0ZSj5Y//+pQVfVe/tQdsYQQy7dwXR30ZfALUIc9PCh9Izir6w==}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-x64-msvc@4.16.1:
+ resolution: {integrity: sha512-x0fvpHMuF7fK5r8oZxSi8VYXkrVmRgubXpO/wcf15Lk3xZ4Jvvh5oG+u7Su1776A7XzVKZhD2eRc4t7H50gL3w==}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@scure/base@1.1.1:
+ resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
+ dev: false
+
+ /@scure/base@1.1.6:
+ resolution: {integrity: sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==}
+ dev: false
+
+ /@scure/bip32@1.3.1:
+ resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==}
+ dependencies:
+ '@noble/curves': 1.1.0
+ '@noble/hashes': 1.3.1
+ '@scure/base': 1.1.6
+ dev: false
+
+ /@scure/bip39@1.2.1:
+ resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
+ dependencies:
+ '@noble/hashes': 1.3.1
+ '@scure/base': 1.1.6
+ dev: false
+
+ /@sveltejs/adapter-auto@3.2.0(@sveltejs/kit@2.5.7):
+ resolution: {integrity: sha512-She5nKT47kwHE18v9NMe6pbJcvULr82u0V3yZ0ej3n1laWKGgkgdEABE9/ak5iDPs93LqsBkuIo51kkwCLBjJA==}
+ peerDependencies:
+ '@sveltejs/kit': ^2.0.0
+ dependencies:
+ '@sveltejs/kit': 2.5.7(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.15)(vite@5.2.10)
+ import-meta-resolve: 4.0.0
+ dev: true
+
+ /@sveltejs/kit@2.5.7(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.15)(vite@5.2.10):
+ resolution: {integrity: sha512-6uedTzrb7nQrw6HALxnPrPaXdIN2jJJTzTIl96Z3P5NiG+OAfpdPbrWrvkJ3GN4CfWqrmU4dJqwMMRMTD/C7ow==}
+ engines: {node: '>=18.13'}
+ hasBin: true
+ requiresBuild: true
+ peerDependencies:
+ '@sveltejs/vite-plugin-svelte': ^3.0.0
+ svelte: ^4.0.0 || ^5.0.0-next.0
+ vite: ^5.0.3
+ dependencies:
+ '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.10)
+ '@types/cookie': 0.6.0
+ cookie: 0.6.0
+ devalue: 5.0.0
+ esm-env: 1.0.0
+ import-meta-resolve: 4.0.0
+ kleur: 4.1.5
+ magic-string: 0.30.10
+ mrmime: 2.0.0
+ sade: 1.8.1
+ set-cookie-parser: 2.6.0
+ sirv: 2.0.4
+ svelte: 4.2.15
+ tiny-glob: 0.2.9
+ vite: 5.2.10
+ dev: true
+
+ /@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.15)(vite@5.2.10):
+ resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==}
+ engines: {node: ^18.0.0 || >=20}
+ peerDependencies:
+ '@sveltejs/vite-plugin-svelte': ^3.0.0
+ svelte: ^4.0.0 || ^5.0.0-next.0
+ vite: ^5.0.0
+ dependencies:
+ '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.10)
+ debug: 4.3.4
+ svelte: 4.2.15
+ vite: 5.2.10
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.10):
+ resolution: {integrity: sha512-sY6ncCvg+O3njnzbZexcVtUqOBE3iYmQPJ9y+yXSkOwG576QI/xJrBnQSRXFLGwJNBa0T78JEKg5cIR0WOAuUw==}
+ engines: {node: ^18.0.0 || >=20}
+ peerDependencies:
+ svelte: ^4.0.0 || ^5.0.0-next.0
+ vite: ^5.0.0
+ dependencies:
+ '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.15)(vite@5.2.10)
+ debug: 4.3.4
+ deepmerge: 4.3.1
+ kleur: 4.1.5
+ magic-string: 0.30.10
+ svelte: 4.2.15
+ svelte-hmr: 0.16.0(svelte@4.2.15)
+ vite: 5.2.10
+ vitefu: 0.2.5(vite@5.2.10)
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@types/cookie@0.6.0:
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+ dev: true
+
+ /@types/estree@1.0.5:
+ resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+ dev: true
+
+ /@types/pug@2.0.10:
+ resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
+ dev: true
+
+ /acorn@8.11.3:
+ resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: true
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: true
+
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+ dev: true
+
+ /anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: true
+
+ /arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+ dev: true
+
+ /aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+ dependencies:
+ dequal: 2.0.3
+ dev: true
+
+ /autoprefixer@10.4.19(postcss@8.4.38):
+ resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ browserslist: 4.23.0
+ caniuse-lite: 1.0.30001612
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.0.0
+ postcss: 8.4.38
+ postcss-value-parser: 4.2.0
+ dev: true
+
+ /axobject-query@4.0.0:
+ resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
+ dependencies:
+ dequal: 2.0.3
+ dev: true
+
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: true
+
+ /binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: true
+
+ /brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: true
+
+ /braces@3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /browserslist@4.23.0:
+ resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+ dependencies:
+ caniuse-lite: 1.0.30001612
+ electron-to-chromium: 1.4.745
+ node-releases: 2.0.14
+ update-browserslist-db: 1.0.13(browserslist@4.23.0)
+ dev: true
+
+ /buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ dev: true
+
+ /bufferutil@4.0.8:
+ resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.8.0
+ dev: false
+
+ /callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /camelcase-css@2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /caniuse-lite@1.0.30001612:
+ resolution: {integrity: sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==}
+ dev: true
+
+ /chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.2
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /code-red@1.0.4:
+ resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@types/estree': 1.0.5
+ acorn: 8.11.3
+ estree-walker: 3.0.3
+ periscopic: 3.1.0
+ dev: true
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: true
+
+ /commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: true
+
+ /cookie@0.6.0:
+ resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ dev: true
+
+ /css-tree@2.3.1:
+ resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+ dependencies:
+ mdn-data: 2.0.30
+ source-map-js: 1.2.0
+ dev: true
+
+ /cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /d@1.0.2:
+ resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
+ engines: {node: '>=0.12'}
+ dependencies:
+ es5-ext: 0.10.64
+ type: 2.7.2
+ dev: false
+
+ /data-uri-to-buffer@4.0.1:
+ resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+ engines: {node: '>= 12'}
+ dev: false
+
+ /debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.0.0
+ dev: false
+
+ /debug@4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+
+ /deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /detect-indent@6.1.0:
+ resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /devalue@5.0.0:
+ resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==}
+ dev: true
+
+ /dexie@3.2.7:
+ resolution: {integrity: sha512-2a+BXvVhY5op+smDRLxeBAivE7YcYaneXJ1la3HOkUfX9zKkE/AJ8CNgjiXbtXepFyFmJNGSbmjOwqbT749r/w==}
+ engines: {node: '>=6.0'}
+ dev: false
+
+ /dexie@4.0.4:
+ resolution: {integrity: sha512-wFzwWSUdi+MC3jiFeQcCp9nInR7EaX8edzYY+4wmiITkQAiSnHpe4Wo2o5Ce5tJZe2nqt7mLW91MsW4GYx3ziQ==}
+ dev: false
+
+ /didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ dev: true
+
+ /dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+ dev: true
+
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: true
+
+ /electron-to-chromium@1.4.745:
+ resolution: {integrity: sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA==}
+ dev: true
+
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: true
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: true
+
+ /es5-ext@0.10.64:
+ resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
+ engines: {node: '>=0.10'}
+ requiresBuild: true
+ dependencies:
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.4
+ esniff: 2.0.1
+ next-tick: 1.1.0
+ dev: false
+
+ /es6-iterator@2.0.3:
+ resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ es6-symbol: 3.1.4
+ dev: false
+
+ /es6-promise@3.3.1:
+ resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
+ dev: true
+
+ /es6-symbol@3.1.4:
+ resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
+ engines: {node: '>=0.12'}
+ dependencies:
+ d: 1.0.2
+ ext: 1.7.0
+ dev: false
+
+ /esbuild@0.20.2:
+ resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.20.2
+ '@esbuild/android-arm': 0.20.2
+ '@esbuild/android-arm64': 0.20.2
+ '@esbuild/android-x64': 0.20.2
+ '@esbuild/darwin-arm64': 0.20.2
+ '@esbuild/darwin-x64': 0.20.2
+ '@esbuild/freebsd-arm64': 0.20.2
+ '@esbuild/freebsd-x64': 0.20.2
+ '@esbuild/linux-arm': 0.20.2
+ '@esbuild/linux-arm64': 0.20.2
+ '@esbuild/linux-ia32': 0.20.2
+ '@esbuild/linux-loong64': 0.20.2
+ '@esbuild/linux-mips64el': 0.20.2
+ '@esbuild/linux-ppc64': 0.20.2
+ '@esbuild/linux-riscv64': 0.20.2
+ '@esbuild/linux-s390x': 0.20.2
+ '@esbuild/linux-x64': 0.20.2
+ '@esbuild/netbsd-x64': 0.20.2
+ '@esbuild/openbsd-x64': 0.20.2
+ '@esbuild/sunos-x64': 0.20.2
+ '@esbuild/win32-arm64': 0.20.2
+ '@esbuild/win32-ia32': 0.20.2
+ '@esbuild/win32-x64': 0.20.2
+ dev: true
+
+ /escalade@3.1.2:
+ resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /esm-env@1.0.0:
+ resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==}
+ dev: true
+
+ /esniff@2.0.1:
+ resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ event-emitter: 0.3.5
+ type: 2.7.2
+ dev: false
+
+ /estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+ dependencies:
+ '@types/estree': 1.0.5
+ dev: true
+
+ /event-emitter@0.3.5:
+ resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ dev: false
+
+ /ext@1.7.0:
+ resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
+ dependencies:
+ type: 2.7.2
+ dev: false
+
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
+ /fastq@1.17.1:
+ resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
+ /fetch-blob@3.2.0:
+ resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+ engines: {node: ^12.20 || >= 14.13}
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 3.3.3
+ dev: false
+
+ /fill-range@7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: true
+
+ /formdata-polyfill@4.0.10:
+ resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+ engines: {node: '>=12.20.0'}
+ dependencies:
+ fetch-blob: 3.2.0
+ dev: false
+
+ /fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ dev: true
+
+ /fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ dev: true
+
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ dev: true
+
+ /glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob@10.3.12:
+ resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.4
+ minipass: 7.0.4
+ path-scurry: 1.10.2
+ dev: true
+
+ /glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+ dev: true
+
+ /globalyzer@0.1.0:
+ resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
+ dev: true
+
+ /globrex@0.1.2:
+ resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+ dev: true
+
+ /graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ dev: true
+
+ /hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ function-bind: 1.1.2
+ dev: true
+
+ /import-fresh@3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+
+ /import-meta-resolve@4.0.0:
+ resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==}
+ dev: true
+
+ /inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+ dev: true
+
+ /inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ dev: true
+
+ /is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.3.0
+ dev: true
+
+ /is-core-module@2.13.1:
+ resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
+ dependencies:
+ hasown: 2.0.2
+ dev: true
+
+ /is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-reference@3.0.2:
+ resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
+ dependencies:
+ '@types/estree': 1.0.5
+ dev: true
+
+ /is-typedarray@1.0.0:
+ resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
+ dev: false
+
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: true
+
+ /jiti@1.21.0:
+ resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
+ hasBin: true
+ dev: true
+
+ /kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /light-bolt11-decoder@3.1.1:
+ resolution: {integrity: sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==}
+ dependencies:
+ '@scure/base': 1.1.1
+ dev: false
+
+ /lilconfig@2.1.0:
+ resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /lilconfig@3.1.1:
+ resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==}
+ engines: {node: '>=14'}
+ dev: true
+
+ /lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ dev: true
+
+ /locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+ dev: true
+
+ /lru-cache@10.2.0:
+ resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
+ engines: {node: 14 || >=16.14}
+ dev: true
+
+ /magic-string@0.30.10:
+ resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ dev: true
+
+ /mdn-data@2.0.30:
+ resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+ dev: true
+
+ /merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /micromatch@4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+
+ /minimatch@9.0.4:
+ resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: true
+
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: true
+
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: true
+
+ /mkdirp@0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.8
+ dev: true
+
+ /mri@1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /mrmime@2.0.0:
+ resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ dev: false
+
+ /ms@2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+
+ /mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+ dev: true
+
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /next-tick@1.1.0:
+ resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
+ dev: false
+
+ /node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ dev: false
+
+ /node-fetch@3.3.2:
+ resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ data-uri-to-buffer: 4.0.1
+ fetch-blob: 3.2.0
+ formdata-polyfill: 4.0.10
+ dev: false
+
+ /node-gyp-build@4.8.0:
+ resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
+ hasBin: true
+ dev: false
+
+ /node-releases@2.0.14:
+ resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
+ dev: true
+
+ /normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /nostr-fetch@0.15.1:
+ resolution: {integrity: sha512-SyXdAsUM3qnuoz35tyX3mwxY1BU5sBsr1JCIMXiV5Ej7uSkyBjnBkzsXH5CYq0p42hI9z6xwfuFqnkvBsaTmJA==}
+ dependencies:
+ '@nostr-fetch/kernel': 0.15.1
+ dev: false
+
+ /nostr-tools@1.17.0(typescript@5.4.5):
+ resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==}
+ peerDependencies:
+ typescript: '>=5.0.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@noble/ciphers': 0.2.0
+ '@noble/curves': 1.1.0
+ '@noble/hashes': 1.3.1
+ '@scure/base': 1.1.1
+ '@scure/bip32': 1.3.1
+ '@scure/bip39': 1.2.1
+ typescript: 5.4.5
+ dev: false
+
+ /object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+ dev: true
+
+ /parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: true
+
+ /path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ dev: true
+
+ /path-scurry@1.10.2:
+ resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.2.0
+ minipass: 7.0.4
+ dev: true
+
+ /periscopic@3.1.0:
+ resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
+ dependencies:
+ '@types/estree': 1.0.5
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+ dev: true
+
+ /picocolors@1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+ dev: true
+
+ /picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /pify@2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /pirates@4.0.6:
+ resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /postcss-import@15.1.0(postcss@8.4.38):
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ postcss: 8.4.38
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.8
+ dev: true
+
+ /postcss-js@4.0.1(postcss@8.4.38):
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.4.38
+ dev: true
+
+ /postcss-load-config@4.0.2(postcss@8.4.38):
+ resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ lilconfig: 3.1.1
+ postcss: 8.4.38
+ yaml: 2.4.1
+ dev: true
+
+ /postcss-load-config@5.0.3(postcss@8.4.38):
+ resolution: {integrity: sha512-90pBBI5apUVruIEdCxZic93Wm+i9fTrp7TXbgdUCH+/L+2WnfpITSpq5dFU/IPvbv7aNiMlQISpUkAm3fEcvgQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ jiti: '>=1.21.0'
+ postcss: '>=8.0.9'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ dependencies:
+ lilconfig: 3.1.1
+ postcss: 8.4.38
+ yaml: 2.4.1
+ dev: true
+
+ /postcss-nested@6.0.1(postcss@8.4.38):
+ resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+ dependencies:
+ postcss: 8.4.38
+ postcss-selector-parser: 6.0.16
+ dev: true
+
+ /postcss-selector-parser@6.0.16:
+ resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+ dev: true
+
+ /postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+ dev: true
+
+ /postcss@8.4.38:
+ resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.0
+ source-map-js: 1.2.0
+ dev: true
+
+ /queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
+ /read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+ dependencies:
+ pify: 2.3.0
+ dev: true
+
+ /readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: true
+
+ /resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /resolve@1.22.8:
+ resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.13.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+ dev: true
+
+ /reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /rimraf@2.7.1:
+ resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /rollup@4.16.1:
+ resolution: {integrity: sha512-5CaD3MPDlPKfhqzRvWXK96G6ELJfPZNb3LHiZxTHgDdC6jvwfGz2E8nY+9g1ONk4ttHsK1WaFP19Js4PSr1E3g==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+ dependencies:
+ '@types/estree': 1.0.5
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.16.1
+ '@rollup/rollup-android-arm64': 4.16.1
+ '@rollup/rollup-darwin-arm64': 4.16.1
+ '@rollup/rollup-darwin-x64': 4.16.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.16.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.16.1
+ '@rollup/rollup-linux-arm64-gnu': 4.16.1
+ '@rollup/rollup-linux-arm64-musl': 4.16.1
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.16.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.16.1
+ '@rollup/rollup-linux-s390x-gnu': 4.16.1
+ '@rollup/rollup-linux-x64-gnu': 4.16.1
+ '@rollup/rollup-linux-x64-musl': 4.16.1
+ '@rollup/rollup-win32-arm64-msvc': 4.16.1
+ '@rollup/rollup-win32-ia32-msvc': 4.16.1
+ '@rollup/rollup-win32-x64-msvc': 4.16.1
+ fsevents: 2.3.3
+ dev: true
+
+ /run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
+ /sade@1.8.1:
+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+ engines: {node: '>=6'}
+ dependencies:
+ mri: 1.2.0
+ dev: true
+
+ /sander@0.5.1:
+ resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
+ dependencies:
+ es6-promise: 3.3.1
+ graceful-fs: 4.2.11
+ mkdirp: 0.5.6
+ rimraf: 2.7.1
+ dev: true
+
+ /set-cookie-parser@2.6.0:
+ resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
+ dev: true
+
+ /shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+ dev: true
+
+ /shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: true
+
+ /sirv@2.0.4:
+ resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
+ engines: {node: '>= 10'}
+ dependencies:
+ '@polka/url': 1.0.0-next.25
+ mrmime: 2.0.0
+ totalist: 3.0.1
+ dev: true
+
+ /sorcery@0.11.0:
+ resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==}
+ hasBin: true
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ buffer-crc32: 0.2.13
+ minimist: 1.2.8
+ sander: 0.5.1
+ dev: true
+
+ /source-map-js@1.2.0:
+ resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: true
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: true
+
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+ dev: true
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: true
+
+ /strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ min-indent: 1.0.1
+ dev: true
+
+ /sucrase@3.35.0:
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ commander: 4.1.1
+ glob: 10.3.12
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.6
+ ts-interface-checker: 0.1.13
+ dev: true
+
+ /supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /svelte-check@3.6.9(postcss-load-config@5.0.3)(postcss@8.4.38)(svelte@4.2.15):
+ resolution: {integrity: sha512-hDQrk3L0osX07djQyMiXocKysTLfusqi8AriNcCiQxhQR49/LonYolcUGMtZ0fbUR8HTR198Prrgf52WWU9wEg==}
+ hasBin: true
+ peerDependencies:
+ svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.25
+ chokidar: 3.6.0
+ fast-glob: 3.3.2
+ import-fresh: 3.3.0
+ picocolors: 1.0.0
+ sade: 1.8.1
+ svelte: 4.2.15
+ svelte-preprocess: 5.1.4(postcss-load-config@5.0.3)(postcss@8.4.38)(svelte@4.2.15)(typescript@5.4.5)
+ typescript: 5.4.5
+ transitivePeerDependencies:
+ - '@babel/core'
+ - coffeescript
+ - less
+ - postcss
+ - postcss-load-config
+ - pug
+ - sass
+ - stylus
+ - sugarss
+ dev: true
+
+ /svelte-hmr@0.16.0(svelte@4.2.15):
+ resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==}
+ engines: {node: ^12.20 || ^14.13.1 || >= 16}
+ peerDependencies:
+ svelte: ^3.19.0 || ^4.0.0
+ dependencies:
+ svelte: 4.2.15
+ dev: true
+
+ /svelte-preprocess@5.1.4(postcss-load-config@5.0.3)(postcss@8.4.38)(svelte@4.2.15)(typescript@5.4.5):
+ resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==}
+ engines: {node: '>= 16.0.0'}
+ requiresBuild: true
+ peerDependencies:
+ '@babel/core': ^7.10.2
+ coffeescript: ^2.5.1
+ less: ^3.11.3 || ^4.0.0
+ postcss: ^7 || ^8
+ postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
+ pug: ^3.0.0
+ sass: ^1.26.8
+ stylus: ^0.55.0
+ sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
+ svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
+ typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ coffeescript:
+ optional: true
+ less:
+ optional: true
+ postcss:
+ optional: true
+ postcss-load-config:
+ optional: true
+ pug:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ typescript:
+ optional: true
+ dependencies:
+ '@types/pug': 2.0.10
+ detect-indent: 6.1.0
+ magic-string: 0.30.10
+ postcss: 8.4.38
+ postcss-load-config: 5.0.3(postcss@8.4.38)
+ sorcery: 0.11.0
+ strip-indent: 3.0.0
+ svelte: 4.2.15
+ typescript: 5.4.5
+ dev: true
+
+ /svelte@4.2.15:
+ resolution: {integrity: sha512-j9KJSccHgLeRERPlhMKrCXpk2TqL2m5Z+k+OBTQhZOhIdCCd3WfqV+ylPWeipEwq17P/ekiSFWwrVQv93i3bsg==}
+ engines: {node: '>=16'}
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/trace-mapping': 0.3.25
+ '@types/estree': 1.0.5
+ acorn: 8.11.3
+ aria-query: 5.3.0
+ axobject-query: 4.0.0
+ code-red: 1.0.4
+ css-tree: 2.3.1
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+ locate-character: 3.0.0
+ magic-string: 0.30.10
+ periscopic: 3.1.0
+ dev: true
+
+ /tailwindcss@3.4.3:
+ resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.2
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.0
+ lilconfig: 2.1.0
+ micromatch: 4.0.5
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.0.0
+ postcss: 8.4.38
+ postcss-import: 15.1.0(postcss@8.4.38)
+ postcss-js: 4.0.1(postcss@8.4.38)
+ postcss-load-config: 4.0.2(postcss@8.4.38)
+ postcss-nested: 6.0.1(postcss@8.4.38)
+ postcss-selector-parser: 6.0.16
+ resolve: 1.22.8
+ sucrase: 3.35.0
+ transitivePeerDependencies:
+ - ts-node
+ dev: true
+
+ /thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+ dependencies:
+ thenify: 3.3.1
+ dev: true
+
+ /thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ dependencies:
+ any-promise: 1.3.0
+ dev: true
+
+ /tiny-glob@0.2.9:
+ resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
+ dependencies:
+ globalyzer: 0.1.0
+ globrex: 0.1.2
+ dev: true
+
+ /to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ dev: true
+
+ /tseep@1.2.1:
+ resolution: {integrity: sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==}
+ dev: false
+
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ dev: true
+
+ /tstl@2.5.16:
+ resolution: {integrity: sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==}
+ dev: false
+
+ /type@2.7.2:
+ resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
+ dev: false
+
+ /typedarray-to-buffer@3.1.5:
+ resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
+ dependencies:
+ is-typedarray: 1.0.0
+ dev: false
+
+ /typescript-lru-cache@2.0.0:
+ resolution: {integrity: sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==}
+ dev: false
+
+ /typescript@5.4.5:
+ resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ /update-browserslist-db@1.0.13(browserslist@4.23.0):
+ resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
+ browserslist: 4.23.0
+ escalade: 3.1.2
+ picocolors: 1.0.0
+ dev: true
+
+ /utf-8-validate@5.0.10:
+ resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.8.0
+ dev: false
+
+ /utf8-buffer@1.0.0:
+ resolution: {integrity: sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ dev: true
+
+ /vite@5.2.10:
+ resolution: {integrity: sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.20.2
+ postcss: 8.4.38
+ rollup: 4.16.1
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /vitefu@0.2.5(vite@5.2.10):
+ resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+ dependencies:
+ vite: 5.2.10
+ dev: true
+
+ /web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+ engines: {node: '>= 8'}
+ dev: false
+
+ /websocket-polyfill@0.0.3:
+ resolution: {integrity: sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==}
+ dependencies:
+ tstl: 2.5.16
+ websocket: 1.0.34
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /websocket@1.0.34:
+ resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ bufferutil: 4.0.8
+ debug: 2.6.9
+ es5-ext: 0.10.64
+ typedarray-to-buffer: 3.1.5
+ utf-8-validate: 5.0.10
+ yaeti: 0.0.6
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: true
+
+ /wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ dev: true
+
+ /yaeti@0.0.6:
+ resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==}
+ engines: {node: '>=0.10.32'}
+ dev: false
+
+ /yaml@2.4.1:
+ resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==}
+ engines: {node: '>= 14'}
+ hasBin: true
+ dev: true
diff --git a/demos/kit-with-idb/postcss.config.cjs b/demos/kit-with-idb/postcss.config.cjs
new file mode 100644
index 00000000..e48cff58
--- /dev/null
+++ b/demos/kit-with-idb/postcss.config.cjs
@@ -0,0 +1,13 @@
+const tailwindcss = require("tailwindcss");
+const autoprefixer = require("autoprefixer");
+
+const config = {
+ plugins: [
+ //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
+ tailwindcss(),
+ //But others, like autoprefixer, need to run after,
+ autoprefixer,
+ ],
+};
+
+module.exports = config;
diff --git a/demos/kit-with-idb/src/app.d.ts b/demos/kit-with-idb/src/app.d.ts
new file mode 100644
index 00000000..743f07b2
--- /dev/null
+++ b/demos/kit-with-idb/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://kit.svelte.dev/docs/types#app
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/demos/kit-with-idb/src/app.html b/demos/kit-with-idb/src/app.html
new file mode 100644
index 00000000..77a5ff52
--- /dev/null
+++ b/demos/kit-with-idb/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/demos/kit-with-idb/src/app.pcss b/demos/kit-with-idb/src/app.pcss
new file mode 100644
index 00000000..1a7b7cf3
--- /dev/null
+++ b/demos/kit-with-idb/src/app.pcss
@@ -0,0 +1,4 @@
+/* Write your global styles here, in PostCSS syntax */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/gui-web/.gitkeep b/demos/kit-with-idb/src/lib/components/RelayFilters.svelte
similarity index 100%
rename from packages/gui-web/.gitkeep
rename to demos/kit-with-idb/src/lib/components/RelayFilters.svelte
diff --git a/demos/kit-with-idb/src/lib/components/RelayGrid.svelte b/demos/kit-with-idb/src/lib/components/RelayGrid.svelte
new file mode 100644
index 00000000..06e80c20
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/components/RelayGrid.svelte
@@ -0,0 +1,30 @@
+
+
+
+{#if items && items?.length}
+
+
+
+{/if}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/components/RelayGridItem.svelte b/demos/kit-with-idb/src/lib/components/RelayGridItem.svelte
new file mode 100644
index 00000000..e79cd624
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/components/RelayGridItem.svelte
@@ -0,0 +1,27 @@
+
+
+{#if icon} {/if}
+{name}
+{relay}
+{seenByCount}
+{createdAt}
+{open}
+{geohash}
+{software}
+{version}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/components/RelayTable.svelte b/demos/kit-with-idb/src/lib/components/RelayTable.svelte
new file mode 100644
index 00000000..b9d0ec7a
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/components/RelayTable.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+
+ icon
+ name
+ Relay
+ Seen By
+ Last Seen
+
+ Open
+ Geohash
+ Software
+ Version
+
+
+{#if $relays?.length}
+{#each $relays as relay}
+
+{/each}
+{/if}
+
+
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/components/RelayTableRow.svelte b/demos/kit-with-idb/src/lib/components/RelayTableRow.svelte
new file mode 100644
index 00000000..f62cbcbd
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/components/RelayTableRow.svelte
@@ -0,0 +1,79 @@
+
+
+
+ {#if icon} {/if}
+ {name}
+ {relay}
+
+
+
+ {seenByCount}
+
+
+
+ {#each seenBy as monitor}
+ { userName(monitor) }
+
+ {/each}
+
+
+
+
+ {#if createdAt}
+
+ {/if}
+ {open}
+ {geohash}
+ {software}
+ {version}
+
+
+
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/contextKeys.ts b/demos/kit-with-idb/src/lib/contextKeys.ts
new file mode 100644
index 00000000..957389a2
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/contextKeys.ts
@@ -0,0 +1,3 @@
+export const NDK_CONTEXT = Symbol('ndk');
+export const SIGNAL_CONTEXT = Symbol('signal');
+export const PROFILES_CONTEXT = Symbol('profiles');
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/fetcher.ts b/demos/kit-with-idb/src/lib/fetcher.ts
new file mode 100644
index 00000000..5225ba79
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/fetcher.ts
@@ -0,0 +1,75 @@
+import NDK, { NDKEvent } from '@nostr-dev-kit/ndk';
+import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie';
+import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
+import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
+import { nHoursAgo } from '$lib/utils';
+import { NDKRelayMeta } from '$lib/relays/relay-meta';
+import { NDKEventGeoCoded } from '$lib/relays/geocoded'
+import type { RelayMetaParsedAll } from '$lib/relays/relay-meta';
+
+export type Result = {
+ data: any,
+ duration: number
+}
+
+export type RttResult = {
+ open: Result,
+ read?: Result,
+ write?: Result
+}
+
+export type RelayData = RelayMetaParsedAll
+
+export class RelayFetcher {
+ private readonly MONITOR: string = "9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6fc9bf923";
+ private dataRelays: string[];
+ private ndk: NDK
+ relayData: Map = new Map();
+ relayEvents: Map = new Map();
+
+ constructor(){
+ this.dataRelays = [
+ "wss://history.nostr.watch",
+ "wss://relaypag.es"
+ ]
+ this.ndk = new NDK({
+ // cacheAdapter: new NDKCacheAdapterDexie({ dbName: 'ndk' }),
+ explicitRelayUrls: normalizeRelayUrlSet(this.dataRelays)
+ });
+ }
+
+ async fetch( cb: (url: string, meta: RelayData ) => {} ){
+ await this.ndk.connect()
+ console.log('connected')
+ const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
+ // const fetcher = NostrFetcher.init()
+ const postIter = fetcher.allEventsIterator(
+ this.dataRelays,
+ { kinds: [ 30066 ], authors: [ this.MONITOR ] },
+ { since: nHoursAgo(2) },
+ { skipVerification: true }
+ );
+ console.log('awaiting')
+ let count = 0
+ for await (const ev of postIter) {
+ const event: NDKRelayMeta = new NDKRelayMeta(this.ndk, ev)
+ const relay = ev.tags.find( tag => tag[0] === "d" )?.[1]
+ if(!relay) continue;
+ try {
+ cb(relay, event.all)
+ } catch(e){
+ throw e
+ }
+
+ };
+ }
+
+ get keys(){
+ return this.relayData.keys()
+ }
+
+ sortByDistance( geohash: string ): Set | undefined {
+ if(this?.relayEvents) return
+ return NDKEventGeoCoded.sortGeospatial(geohash, new Set(this.relayEvents.values()))
+ }
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/fetchers/monitorRelays.ts b/demos/kit-with-idb/src/lib/fetchers/monitorRelays.ts
new file mode 100644
index 00000000..d7700e98
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/fetchers/monitorRelays.ts
@@ -0,0 +1,71 @@
+import { browser } from '$app/environment';
+
+import NDK, { type NDKFilter } from "@nostr-dev-kit/ndk";
+
+import { MonitorManager } from "@nostrwatch/kit";
+import { MonitorCacheIdbAdapter, RelayCacheIdbAdapter } from "@nostrwatch/kit-adapter-idb";
+import { relayDb, liveQuery } from '@nostrwatch/idb';
+
+import { storable } from '$lib/stores/localStorageStore';
+
+let primaryMonitorPubkey
+const explicitRelayUrls = ['wss://purplepag.es', 'wss://relaypag.es', 'wss://history.nostr.watch'];
+
+export const monitorRelays = liveQuery(() => relayDb.checks.where({primaryMonitorPubkey}).toArray());
+export const knownRelays = liveQuery(() => relayDb.relays.toArray());
+
+// knownRelays.subscribe(async (relays) => {
+// console.log(relays)
+// relayStore.set(relays);
+// const _checkStore = {};
+// const _checkCountStore = {};
+// relays.forEach(async (relay) => {
+// const {relay:url} = relay
+// _checkStore[url] = getRelayChecksByMonitor(primaryMonitorPubkey);
+// _checkCountStore[url] = getRelayMonitorCounts(url)
+// });
+// checkStore.set(_checkStore);
+// checkCountStore.set(_checkCountStore);
+// });
+
+export const getRelayChecksByMonitor = async (monitorPubkey: string) => {
+ return liveQuery(() => relayDb.checks.where({monitorPubkey}).toArray());
+}
+
+export const getRelayMonitorCounts = async (relay: string) => {
+ // return liveQuery(() => relayDb.checks.where({relay}).count());
+ return relayDb.checks.where({relay}).count()
+ }
+
+const statToFilter = (stats): Record => {
+ const filter: Record = {};
+ Object.keys(stats).forEach(key => {
+ const since = stats[key]?.mostRecent
+ if(!since) return
+ filter[key] = { since }
+ })
+ return filter
+}
+
+export const initRelayData = async (ndk: NDK, _primaryMonitorPubkey: string): Promise => {
+ primaryMonitorPubkey = _primaryMonitorPubkey
+ // const lastCheckStore = storable('monitorStat');
+ if (!browser) return console.warn('not in browser.');
+ console.log('mounted');
+
+ console.log('monitors init');
+ const monitors = new MonitorManager(
+ ndk,
+ {
+ primaryMonitor: primaryMonitorPubkey,
+ monitorCache: new MonitorCacheIdbAdapter(ndk),
+ relayCache: new RelayCacheIdbAdapter(ndk)
+ },
+ undefined
+ );
+ const stats = await monitors.init()
+ console.log('stats', stats)
+ // await monitors.populateRelays(new Set([await monitors.cache.get(primaryMonitorPubkey)]))
+ // lastCheckStore.set(stats)
+ return monitors
+}
diff --git a/demos/kit-with-idb/src/lib/fetchers/profiles.ts b/demos/kit-with-idb/src/lib/fetchers/profiles.ts
new file mode 100644
index 00000000..eac898fa
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/fetchers/profiles.ts
@@ -0,0 +1,24 @@
+import { NDK_CONTEXT } from "$lib/contextKeys";
+import { profileStore } from "$lib/stores/profileStore";
+import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
+import type NDK from "@nostr-dev-kit/ndk";
+import { monitorDb } from "@nostrwatch/idb";
+import { getContext } from "svelte";
+
+let profiles: Map = new Map()
+
+profileStore.subscribe( store => profiles = store )
+
+export const updateProfiles = async (ndk: NDK) => {
+ const monitors = await monitorDb.monitors.toArray()
+ const profiles = new Map()
+ monitors.forEach(async (monitor) => {
+ const pubkey = monitor.monitorPubkey
+ if(profiles.get(pubkey)) return
+ const user = ndk.getUser({pubkey})
+ await user.fetchProfile()
+ profiles.set(pubkey, user.profile)
+ console.log('profile', user.profile)
+ });
+ profileStore.set(profiles)
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/loadWorker.ts b/demos/kit-with-idb/src/lib/loadWorker.ts
new file mode 100644
index 00000000..2d734cc0
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/loadWorker.ts
@@ -0,0 +1,7 @@
+// src/lib/loadWorker.ts
+export async function loadWorker() {
+ console.log('Loading worker...');
+ const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
+ console.log('Worker loaded:', worker);
+ return worker;
+}
diff --git a/demos/kit-with-idb/src/lib/routines/countChecksPerRelay.ts b/demos/kit-with-idb/src/lib/routines/countChecksPerRelay.ts
new file mode 100644
index 00000000..3cd19bd1
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/countChecksPerRelay.ts
@@ -0,0 +1,12 @@
+import { checkStatStore } from "$lib/stores"
+import { relayDb } from "@nostrwatch/idb"
+
+export const countChecksPerRelay = async () => {
+ const relays = await relayDb.relays.toArray()
+ relays.forEach( async (record) => {
+ const { relay } = record
+ const checks = await relayDb.checks.where({ relay }).toArray()
+ const pubkeys = checks.map(check => check.monitorPubkey)
+ checkStatStore.update(store => store.set(relay, pubkeys))
+ })
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/localCheck.ts b/demos/kit-with-idb/src/lib/routines/localCheck.ts
new file mode 100644
index 00000000..bb899d5d
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/localCheck.ts
@@ -0,0 +1,29 @@
+import { queueStore } from "$lib/stores/QueueStore";
+import { relayDb, type ICheck } from "@nostrwatch/idb";
+import { relayStore } from "$lib/stores/relayStore";
+
+export let localPing = new Map()
+
+export const initLocalCheck = async () => {
+ const relays = await relayDb.relays.toArray();
+
+ relays.forEach( async (relay) => {
+
+ const { relay: url } = relay;
+ console.log('add task', relay)
+ const { result } = await queueStore.addTask(url);
+ console.log('finish task', result)
+
+ localPing.set(url, result);
+
+ const record: ICheck = {
+ nid: 'na',
+ monitorPubkey: 'currentuser',
+ relay: url,
+ open: result,
+ createdAt: Math.round(new Date().getTime()/1000)
+ }
+ await relayDb.checks.put(record);
+ console.log('user check', (await relayDb.checks.where({relay: url, monitorPubkey: 'currentuser' }).first())?.open)
+ })
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateChecks.ts b/demos/kit-with-idb/src/lib/routines/populateChecks.ts
new file mode 100644
index 00000000..8038095f
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateChecks.ts
@@ -0,0 +1,17 @@
+import { NDKEvent } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+
+export const populateChecks = async ( monitorPubkey: string): Promise => {
+ const events = await relayDb.events.where({monitorPubkey}).toArray()
+ let mostRecent = 0
+ events.forEach(async (event) => {
+ const check = (await RelayTransform( event.event ))?.check
+ if(!check) return
+ if(check) {
+ if(check.createdAt > mostRecent) mostRecent = check.createdAt
+ console.log(`adding check ${check.nid}`)
+ await relayDb.checks.put(check)
+ }
+ })
+ return mostRecent
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateEvents.ts b/demos/kit-with-idb/src/lib/routines/populateEvents.ts
new file mode 100644
index 00000000..5c5a4b30
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateEvents.ts
@@ -0,0 +1,19 @@
+import NDK, { NDKKind, NDKRelayMonitor,NDKRelayMeta } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+
+export const populateEvents = async ( monitor: NDKRelayMonitor ): Promise => {
+ console.log(monitor.ndk)
+ const filter = monitor.nip66Filter([NDKKind.RelayMeta], {limit: 1000})
+ console.log('filter', filter)
+ const events = await monitor.ndk.fetchEvents(filter)
+ let mostRecent = 0
+ let count = 1
+ console.log('events', events.size)
+ events.forEach(async (event: NDKRelayMeta) => {
+ if(event.created_at > mostRecent) mostRecent = event.created_at
+ const _event = (await RelayTransform(event.rawEvent()))?.event
+ await relayDb.events.put(_event)
+ console.log(`${count++}/${events.size} event added ${event.id}`)
+ })
+ return mostRecent
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateGeocodes.ts b/demos/kit-with-idb/src/lib/routines/populateGeocodes.ts
new file mode 100644
index 00000000..96f58a2b
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateGeocodes.ts
@@ -0,0 +1,16 @@
+import { NDKEvent } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+
+export const populateGeocodes = async ( monitorPubkey: string): Promise => {
+ const events = await relayDb.events.where({monitorPubkey}).toArray()
+ let mostRecent = 0
+ events.forEach(async (event) => {
+ const geocode = (await RelayTransform( event.event ))?.geocodes
+ if(!geocode) return
+ if(geocode) {
+ console.log(`adding geocode ${geocode.nid}`)
+ await relayDb.geocodes.put(geocode)
+ }
+ })
+ return mostRecent
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateNip11s.ts b/demos/kit-with-idb/src/lib/routines/populateNip11s.ts
new file mode 100644
index 00000000..78f6207c
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateNip11s.ts
@@ -0,0 +1,18 @@
+import { NDKEvent } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+import { nip11Store } from "../stores/nip11Store";
+
+export const populateNip11s = async ( monitorPubkey: string): Promise => {
+ const events = await relayDb.events.where({monitorPubkey}).toArray()
+ let mostRecent = 0
+ events.forEach(async (event) => {
+ const nip11 = (await RelayTransform( event.event ))?.nip11
+ if(!nip11) return
+ if(nip11) {
+ if(nip11.createdAt > mostRecent) mostRecent = nip11.createdAt
+ console.log(`adding nip11 ${nip11.nid}`)
+ await relayDb.nip11s.put(nip11)
+ }
+ })
+ return mostRecent
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateRelays.ts b/demos/kit-with-idb/src/lib/routines/populateRelays.ts
new file mode 100644
index 00000000..5e7f5ec1
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateRelays.ts
@@ -0,0 +1,17 @@
+import { NDKEvent } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+
+export const populateRelays = async ( monitorPubkey: string): Promise => {
+ const events = await relayDb.events.where({monitorPubkey}).toArray()
+ let mostRecent = 0
+ events.forEach(async (event) => {
+ const relay = (await RelayTransform( event.event ))?.relay
+ if(!relay) return
+ if(relay) {
+ if(relay.lastSeen > mostRecent) mostRecent = relay.lastSeen
+ console.log(`adding relay ${relay.relay}`)
+ await relayDb.relays.put(relay)
+ }
+ })
+ return mostRecent
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/populateSsls.ts b/demos/kit-with-idb/src/lib/routines/populateSsls.ts
new file mode 100644
index 00000000..f265afb1
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/populateSsls.ts
@@ -0,0 +1,22 @@
+// import { NostrEvent } from "@nostr-dev-kit/ndk";
+import { relayDb, RelayTransform } from "@nostrwatch/idb";
+
+export const populateSsls = async ( monitorPubkey: string): Promise => {
+ const events = await relayDb.events.where({monitorPubkey}).toArray()
+ let mostRecent = 0
+ events.forEach(async (event) => {
+ const timestamp = await putSsl(event.event)
+ if(timestamp && timestamp > mostRecent) mostRecent = timestamp
+ })
+ return mostRecent
+};
+
+export const putSsl = async (event): Promise => {
+ const ssl = (await RelayTransform( event ))?.ssl
+ if(!ssl) return
+ if(ssl) {
+ console.log(`adding ssl ${ssl.nid}`)
+ await relayDb.ssls.put(ssl)
+ return ssl.createdAt
+ }
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/queue.ts b/demos/kit-with-idb/src/lib/routines/queue.ts
new file mode 100644
index 00000000..66c194ea
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/queue.ts
@@ -0,0 +1,52 @@
+import fastq from 'fastq';
+import type { queueAsPromised } from 'fastq'
+
+import { queueStore } from '$lib/stores/QueueStore';
+
+import { Nocap } from "@nostrwatch/nocap"
+import WebsocketAdapter from "@nostrwatch/nocap-websocket-browser-adapter-default"
+import Nip11Adapter from "@nostrwatch/nocap-info-adapter-default"
+
+
+
+type Task = (arg: any) => Promise;
+
+class QueueWrapper {
+ private queue: queueAsPromised;
+
+ constructor(worker: Task, concurrency: number) {
+ this.queue = fastq.promise(worker, concurrency);
+ }
+
+ addTask(taskData: any): Promise {
+ return this.queue.push(taskData);
+ }
+
+ getQueue() {
+ return this.queue;
+ }
+}
+
+export default QueueWrapper;
+
+export const initWorkers = async () => {
+ await queueStore.registerWorker(nocapWorker, 20);
+};
+
+const nocapWorker = async (url: string) => {
+ console.log('working.')
+ const nocap = new Nocap(url);
+ nocap.on('error', (err) => {
+ console.error('error', err)
+ })
+
+ await nocap.useAdapter(WebsocketAdapter)
+ await nocap.useAdapter(Nip11Adapter)
+
+ console.log('adapter set', nocap.adapters.websocket)
+ const check = await nocap.check(['open']);
+ console.log('check complete')
+ const { duration:result } = check.open;
+
+ return { url, result };
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/routines/relayRoutines.ts b/demos/kit-with-idb/src/lib/routines/relayRoutines.ts
new file mode 100644
index 00000000..386ebaac
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/routines/relayRoutines.ts
@@ -0,0 +1,13 @@
+import { eventsPurgeAge, liveQuery, monitorDb } from "@nostrwatch/idb"
+import { monitorStore } from "$lib/stores"
+import type { NDKRelayMonitor } from "@nostr-dev-kit/ndk"
+
+let monitors = new Set()
+
+export const relayRoutines = {
+ monitors.keys().forEach(async (monitorPubkey) => {
+ monitors.get(monitorPubkey).frequency
+ eventsPurgeAge();
+ })
+}
+
diff --git a/demos/kit-with-idb/src/lib/stores/LocalPing.ts b/demos/kit-with-idb/src/lib/stores/LocalPing.ts
new file mode 100644
index 00000000..4cd82253
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/LocalPing.ts
@@ -0,0 +1,25 @@
+import { writable } from 'svelte/store';
+import { relayDb, liveQuery, type Observable } from '@nostrwatch/idb';
+import { queueStore } from '$lib/stores/QueueStore';
+
+const localPing = writable(new Map());
+
+async function fetchPingTimes() {
+ const relays = await relayDb.relays.toArray();
+
+ relays.forEach(async (relay) => {
+ const { relay: url } = relay;
+ const { result } = await queueStore.addTask(url);
+
+ localPing.update(map => {
+ const newMap = new Map(map); // Create a copy of the current map
+ newMap.set(url, result.duration); // Update the map with new data
+ return newMap; // Return the updated map
+ });
+ });
+}
+
+// Ensure this function is called somewhere in your component lifecycle
+fetchPingTimes();
+
+export { localPing };
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/QueueStore.ts b/demos/kit-with-idb/src/lib/stores/QueueStore.ts
new file mode 100644
index 00000000..73c31eb3
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/QueueStore.ts
@@ -0,0 +1,42 @@
+// src/lib/stores/queueStore.ts
+import { writable, get } from 'svelte/store';
+import QueueWrapper from '$lib/routines/queue';
+
+export const queueContextKey = Symbol('queueContext');
+
+export interface QueueStore {
+ subscribe: (
+ this: void,
+ run: import('svelte/store').Subscriber,
+ invalidate?: import('svelte/store').Invalidator | undefined
+ ) => import('svelte/store').Unsubscriber;
+ registerWorker: (worker: (arg: any) => Promise, concurrency?: number) => void;
+ addTask: (data: any) => Promise;
+
+}
+
+const createQueueStore = (): QueueStore => {
+ const { subscribe, set } = writable(null);
+
+ const registerWorker = (worker: (arg: any) => Promise, concurrency: number = 1) => {
+ const queue = new QueueWrapper(worker, concurrency);
+ set(queue);
+ };
+
+ const addTask = async (data: any): Promise => {
+ const queueWrapper = get(queueStore); // Accessing the current value of queueStore
+ if (queueWrapper) {
+ return queueWrapper.addTask(data);
+ }
+ throw new Error('QueueWrapper is not initialized.');
+ };
+
+ return {
+ subscribe,
+ registerWorker,
+ addTask
+ };
+};
+
+export const queueStore = createQueueStore();
+export const Queue = QueueWrapper;
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/RelayStore.ts b/demos/kit-with-idb/src/lib/stores/RelayStore.ts
new file mode 100644
index 00000000..3e4951d8
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/RelayStore.ts
@@ -0,0 +1,96 @@
+// import { relayDb, type IRelay } from '@nostrwatch/idb';
+// import { configureCustomStorageType, persisted } from '@square/svelte-store';
+
+// export type RelaysDbKey = Record;
+
+// const defaultIRelay = { relay: "", lastSeen: -1, network: ""}
+
+// const storageType = 'IDB.RELAYS'
+
+// configureCustomStorageType(storageType, {
+// getStorageItem: (key: string): Promise => {
+// return relayDb.relays.get(JSON.parse(key))
+// },
+// setStorageItem: (key: string, value: IRelay): Promise => {
+// return relayDb.relays.put(value)
+// },
+// removeStorageItem: (key) => {
+// return relayDb.relays.delete(key)
+// },
+// });
+
+// export const relays = (defaultValue: IRelay = defaultIRelay, key: RelaysDbKey) => {
+// return persisted(
+// defaultValue,
+// JSON.stringify(key),
+// { storageType, reloadable: true }
+// );
+// }
+
+
+import { writable, get, type Writable } from 'svelte/store';
+
+import { relayDb, liveQuery, type IRelay } from '@nostrwatch/idb';
+// import { checksStore, getRelayMonitorCounts, } from './checksStore';
+import { checksStore, nip11Store, checkStatStore } from './index';
+
+export type RelayStore = IRelay[]
+const relayData: Writable = writable([]);
+
+liveQuery( () => relayDb.relays.toArray()).subscribe( (_relays: IRelay[]) => relayData.set(_relays) );
+
+export function relays() {
+ const { subscribe, set:_set, update } = relayData
+
+ const sync = (): Promise => {
+ return new Promise (async (resolve, reject) => {
+ try {
+ const data: RelayStore = await relayDb.relays.toArray()
+ resolve(set(data));
+ } catch (error) {
+ console.error(error);
+ reject (error);
+ }
+ })
+ }
+
+ const set = async (relays:RelayStore) => {
+ relayDb.relays.bulkPut(relays)
+ _set(relays);
+ }
+
+ return {
+ subscribe,
+ set,
+ update,
+ sync
+ }
+}
+
+
+// const checksStore = checks();
+
+const primaryMonitorPubkey = "9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6fc9bf923"
+// export const relayStore = writable([]);
+export const monitorRelays = liveQuery(() => relayDb.checks.where({primaryMonitorPubkey}).toArray());
+export const relayStore = liveQuery(() => relayDb.relays.toArray() );
+
+// checksStore.subscribe(checks => {
+
+// })
+
+const updateRelays = async (relays: IRelay[]) => {
+ relays.forEach((relay) => {
+ const { relay: url } = relay; // Assuming relay object has 'relay' property to be used as URL
+ // checksStore.sync(url, primaryMonitorPubkey);
+ // nip11Store.sync(url, primaryMonitorPubkey);
+ });
+}
+
+relayStore.subscribe( updateRelays );
+
+export const populateRelaysStore = () => {
+ // relayDb.relays.toArray().then(updateRelays);
+}
+
+// setContext('relays', relayStore);
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/checkCountStore.ts b/demos/kit-with-idb/src/lib/stores/checkCountStore.ts
new file mode 100644
index 00000000..457bcdc2
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/checkCountStore.ts
@@ -0,0 +1,20 @@
+import { writable, type Writable } from 'svelte/store';
+import { relayDb, liveQuery, type ICheck } from '@nostrwatch/idb';
+
+export const checkStatStore = writable({});
+
+export function getRelayMonitorCounts(relay: string) {
+ relayDb.checks
+ .where({ relay })
+ .toArray()
+ .then(results => {
+ const uniquePubkeys = new Set(results.map(check => check.monitorPubkey));
+ checkStatStore.update(store => {
+ return { ...store, [relay]: uniquePubkeys.size };
+ });
+ })
+ .catch(error => {
+ console.error(`Error fetching monitor counts for ${relay}:`, error);
+ });
+}
+
diff --git a/demos/kit-with-idb/src/lib/stores/checkStatStore.ts b/demos/kit-with-idb/src/lib/stores/checkStatStore.ts
new file mode 100644
index 00000000..b841b13c
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/checkStatStore.ts
@@ -0,0 +1,47 @@
+import { writable, type Writable } from 'svelte/store';
+import { relayDb, liveQuery, type ICheck } from '@nostrwatch/idb';
+
+import { preferencesStore } from './preferencesStore';
+// export * from '$lib/stores/checkCountStore';
+
+export type CheckStatStore = Writable>;
+
+interface CheckStatInterface {
+ subscribe: CheckStatStore['subscribe'];
+ update: CheckStatStore['update'];
+ set: CheckStatStore['set'];
+ sync: (relay: string) => Promise;
+}
+
+const checkStats = (): CheckStatInterface => {
+ const store: CheckStatStore = writable(new Map());
+ const { subscribe, update, set } = store;
+ let monitorPubkey: string | undefined;
+
+ preferencesStore.subscribe(preferences => {
+ monitorPubkey = preferences.primaryMonitorPubkey;
+ });
+
+ const sync = async (relay: string) => {
+ relayDb.checks
+ .where({ relay })
+ .toArray()
+ .then(results => {
+ const uniquePubkeys = Array.from(new Set(results.map(check => check.monitorPubkey)));
+ store.update(store => store.set(relay, uniquePubkeys));
+ })
+ .catch(error => {
+ console.error(`Error fetching monitor counts for ${relay}:`, error);
+ });
+ };
+
+ return {
+ subscribe,
+ update,
+ set,
+ sync,
+ };
+};
+
+
+export const checkStatStore = checkStats();
diff --git a/demos/kit-with-idb/src/lib/stores/checksStore.ts b/demos/kit-with-idb/src/lib/stores/checksStore.ts
new file mode 100644
index 00000000..a7abcd4c
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/checksStore.ts
@@ -0,0 +1,86 @@
+import { writable, type Writable } from 'svelte/store';
+import { liveQuery, relayDb, type ICheck } from '@nostrwatch/idb';
+import { preferencesStore } from './preferencesStore';
+import { checkStatStore } from './checkStatStore';
+import { storeKey } from '.';
+// export * from '$lib/stores/checkCountStore';
+
+export type CheckStore = Writable>;
+
+
+
+interface ChecksInterface {
+ subscribe: CheckStore['subscribe'];
+ update: CheckStore['update'];
+ set: CheckStore['set'];
+ sync: (relay: string, monitorPubkey: string) => Promise;
+}
+
+const checks = (): ChecksInterface => {
+ const store: CheckStore = writable(new Map());
+ const { subscribe, update, set } = store;
+ let monitorPubkey: string | undefined;
+
+ preferencesStore.subscribe(preferences => {
+ monitorPubkey = preferences.primaryMonitor;
+ });
+
+ const populate = async () => {
+ const record = await relayDb.checks.toArray();
+ if (record) {
+ const records = new Map()
+ record.forEach(check => {
+ update(store => store.set(check.relay, check));
+ });
+ }
+ }
+
+ const sync = async (relay: string, monitorPubkey: string) => {
+ const record = await relayDb.checks.where({ relay, monitorPubkey }).first();
+ if (record) {
+ update(store => store.set(storeKey(relay, monitorPubkey), record));
+ }
+ };
+
+ return {
+ subscribe,
+ update,
+ set,
+ sync,
+ };
+};
+
+const updateChecks = (checks: ICheck[]) => {
+ checksStore.update(store => {
+ const updatedStore = new Map(store);
+ checks.forEach(check => {
+ updatedStore.set(storeKey(check.relay, check.monitorPubkey), check);
+ // checkStatStore.sync(check.relay);
+ });
+ return updatedStore;
+ });
+}
+
+liveQuery( () => relayDb.checks.toArray() ).subscribe(updateChecks);
+
+export const populateChecksStore = () => {
+ relayDb.checks.toArray().then(updateChecks);
+}
+
+export const checksStore = checks();
+
+// export function getRelayCheckByMonitor(relay: string, monitsrPubkey: string) {
+// relayDb.checks.where({ relay, monitorPubkey }).first()
+// .then(result => {
+// if (result) {
+// checkStore.update(store => {
+// const updatedStore = new Map(store);
+// updatedStore.set(relay, result);
+// return updatedStore;
+// });
+// }
+// })
+// .catch(error => {
+// console.error(`Error fetching check data for ${relay}:`, error);
+// });
+// }
diff --git a/demos/kit-with-idb/src/lib/stores/index.ts b/demos/kit-with-idb/src/lib/stores/index.ts
new file mode 100644
index 00000000..c60195b1
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/index.ts
@@ -0,0 +1,22 @@
+export * from './relayStore'
+export * from './checksStore'
+export * from './monitorStore'
+export * from './checkStatStore'
+export * from './localCheckStore'
+export * from './nip11Store'
+export * from './preferencesStore'
+
+export * from './utils'
+
+import { preferencesStore } from './preferencesStore'
+
+export let preferences = {};
+
+preferencesStore.subscribe(_preferences => {
+ preferences = _preferences;
+})
+
+export const storeKey = (relay: string, monitorPubkey?: string) => {
+ if(!preferences?.primaryMonitor) alert('none')
+ return `${monitorPubkey || preferences.primaryMonitor}:${relay}`
+}
\ No newline at end of file
diff --git a/packages/nocap/.eslintrc.yml b/demos/kit-with-idb/src/lib/stores/loaderStore.ts
similarity index 100%
rename from packages/nocap/.eslintrc.yml
rename to demos/kit-with-idb/src/lib/stores/loaderStore.ts
diff --git a/demos/kit-with-idb/src/lib/stores/localCheckStore.ts b/demos/kit-with-idb/src/lib/stores/localCheckStore.ts
new file mode 100644
index 00000000..f4a7bc19
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/localCheckStore.ts
@@ -0,0 +1,14 @@
+import { writable } from 'svelte/store';
+
+import { relayDb, liveQuery } from '@nostrwatch/idb';
+
+export const localCheckStore = writable({});
+
+export function getLocalCheckRtt(relay) {
+ const result = relayDb.checks.where({relay, monitorPubkey: 'currentuser'})
+ if (result) {
+ localCheckStore.update(store => {
+ return { ...store, [relay]: result };
+ });
+ }
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/localStorageStore.ts b/demos/kit-with-idb/src/lib/stores/localStorageStore.ts
new file mode 100644
index 00000000..bb50b995
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/localStorageStore.ts
@@ -0,0 +1,26 @@
+import { get, writable } from 'svelte/store'
+
+export function storable(key: string) {
+ const store = writable({});
+ const { subscribe, set } = store;
+ const isBrowser = typeof window !== 'undefined';
+
+ isBrowser &&
+ localStorage[key] &&
+ set(JSON.parse(localStorage[key]));
+
+ return {
+ subscribe,
+ get: () => get(store),
+ set: n => {
+ isBrowser && (localStorage[key] = JSON.stringify(n));
+ set(n);
+ },
+ update: cb => {
+ const updatedStore = cb(get(store));
+
+ isBrowser && (localStorage[key] = JSON.stringify(updatedStore));
+ set(updatedStore);
+ }
+ };
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/monitorStore.ts b/demos/kit-with-idb/src/lib/stores/monitorStore.ts
new file mode 100644
index 00000000..557fe33d
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/monitorStore.ts
@@ -0,0 +1,79 @@
+import { derived, writable } from 'svelte/store';
+
+import { monitorDb, liveQuery, type IMonitor } from '@nostrwatch/idb';
+import { getContext, setContext } from 'svelte';
+import NDK, { NDKRelayMonitor } from '@nostr-dev-kit/ndk';
+import { storeKey } from '.';
+import { NDK_CONTEXT } from '$lib/contextKeys';
+
+export type MonitorStore = Map
+
+interface MonitorInterface {
+ subscribe: MonitorStore['subscribe'];
+ update: MonitorStore['update'];
+ set: MonitorStore['set'];
+ sync: (monitorPubkey: string) => Promise;
+}
+
+const monitorData = writable(new Map());
+
+liveQuery( () => monitorDb.monitors.toArray()).subscribe( (_monitors: IMonitor[]) => monitorData.set(dbToMap(_monitors)) );
+
+export function monitors() {
+ const { subscribe, set, update } = monitorData
+
+ const sync = (): Promise => {
+ return new Promise (async (resolve, reject) => {
+ try {
+ const data: MonitorStore = dbToMap(await monitorDb.monitors.toArray())
+ resolve(set(data));
+ } catch (error) {
+ console.error(error);
+ reject (error);
+ }
+ })
+ }
+
+ return {
+ subscribe,
+ set,
+ update,
+ sync
+ }
+}
+
+const dbToMap = (monitors: IMonitor[]): MonitorStore => {
+ return monitors.reduce(function(map, monitor: IMonitor) {
+ map[monitor.monitorPubkey] = monitor;
+ return map;
+ }, new Map());
+}
+
+const updateMonitors = (monitors: IMonitor[]) => {
+ monitorsStore.update(store => {
+ monitors.forEach(monitor => {
+ store.set(monitor.monitorPubkey, monitor);
+ });
+ return store;
+ });
+}
+
+export const monitorsStore = monitors();
+
+liveQuery( () => monitorDb.monitors.toArray() ).subscribe(updateMonitors);
+
+// export const monitorProfiles = derived(
+// monitorsStore,
+// ($monitorsStore) => {
+// const profiles = new Map()
+// const ndk: NDK = getContext(NDK_CONTEXT)
+// Array.from($monitorsStore.keys()).forEach(async (pubkey) => {
+// const user = await ndk.getUser(pubkey)
+// profiles.set(pubkey, await user.fetchProfile())
+// })
+// return profiles
+// }
+// );
+
+
+
diff --git a/demos/kit-with-idb/src/lib/stores/nip11Store.ts b/demos/kit-with-idb/src/lib/stores/nip11Store.ts
new file mode 100644
index 00000000..2d024322
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/nip11Store.ts
@@ -0,0 +1,59 @@
+import { writable, type Writable } from 'svelte/store';
+import { liveQuery, relayDb, type INip11 } from '@nostrwatch/idb';
+import { preferencesStore } from './preferencesStore';
+import { storeKey } from '.';
+// import { preferencesStore } from './preferencesStore';
+
+
+export type Nip11Store = Writable>;
+
+interface Nip11Interface {
+ subscribe: Nip11Store['subscribe'];
+ update: Nip11Store['update'];
+ set: Nip11Store['set'];
+ sync: (relay: string, monitorPubkey: string) => Promise;
+}
+
+const nip11s = (): Nip11Interface => {
+ const store: Nip11Store = writable(new Map());
+ const { subscribe, update, set } = store;
+ // let monitorPubkey: string | undefined;
+
+ // preferencesStore.subscribe(preferences => {
+ // monitorPubkey = preferences.primaryMonitorPubkey;
+ // });
+
+ const sync = async (relay: string, monitorPubkey: string) => {
+ const record = await relayDb.nip11s.where({ relay, monitorPubkey }).first();
+ if (record) {
+ update(store => store.set(storeKey(relay, monitorPubkey), record));
+ }
+ };
+
+ return {
+ subscribe,
+ update,
+ set,
+ sync,
+ };
+};
+
+let preferences;
+preferencesStore.subscribe(_preferences => {
+ preferences = _preferences;
+});
+
+const updateNip11s = (nip11s: INip11[]) => {
+ nip11Store.update(store => {
+ const updatedStore = new Map(store);
+ nip11s.forEach(nip11 => {
+ updatedStore.set(storeKey(nip11.relay, nip11.monitorPubkey), nip11);
+ });
+ return updatedStore;
+ });
+}
+
+liveQuery( () => relayDb.nip11s.toArray() ).subscribe( updateNip11s );
+
+
+export const nip11Store = nip11s();
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/preferencesStore.ts b/demos/kit-with-idb/src/lib/stores/preferencesStore.ts
new file mode 100644
index 00000000..93ad0824
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/preferencesStore.ts
@@ -0,0 +1,22 @@
+import { setContext } from 'svelte';
+import { writable } from 'svelte/store';
+import type { preferences } from '.';
+
+export const preferencesStore = writable({});
+
+export type Preferences = {
+ primaryMonitor?: string;
+ explicitRelayUrls?: string[];
+}
+
+const { update } = preferencesStore;
+
+export const setPreference = ( key: string, value: any) => {
+ update( store => {
+ store[key] = value;
+ return store;
+ });
+}
+
+setPreference('primaryMonitor', 'abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832');
+setPreference('explicitRelayUrls', ['wss://purplepag.es', 'wss://user.kindpag.es', 'wss://relaypag.es', 'wss://history.nostr.watch'])
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/stores/profileStore.ts b/demos/kit-with-idb/src/lib/stores/profileStore.ts
new file mode 100644
index 00000000..e1023510
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/profileStore.ts
@@ -0,0 +1,25 @@
+import NDK from "@nostr-dev-kit/ndk";
+import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
+import { liveQuery, monitorDb } from "@nostrwatch/idb";
+import { writable } from "svelte/store";
+
+export type ProfileStore = Map
+
+const profileData = writable(new Map());
+
+export function profiles() {
+ const { subscribe, set, update } = profileData
+
+ const sync = async (): Promise => {}
+
+ return {
+ subscribe,
+ set,
+ update,
+ sync
+ }
+}
+
+export const profileStore = profiles()
+
+// liveQuery( () => monitorDb.monitors.toArray()).subscribe( () => profileStore.sync() );
diff --git a/demos/kit-with-idb/src/lib/stores/utils.ts b/demos/kit-with-idb/src/lib/stores/utils.ts
new file mode 100644
index 00000000..2ed3854f
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/stores/utils.ts
@@ -0,0 +1,7 @@
+import { populateChecksStore } from './checksStore'
+import { populateRelaysStore } from './relayStore'
+
+export const populateStores = () => {
+ populateRelaysStore()
+ populateChecksStore()
+}
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/testWorker.ts b/demos/kit-with-idb/src/lib/testWorker.ts
new file mode 100644
index 00000000..b4aafba1
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/testWorker.ts
@@ -0,0 +1,19 @@
+// src/lib/testWorker.ts
+import { loadWorker } from './loadWorker';
+
+async function testWorker() {
+ try {
+ const worker = await loadWorker();
+
+ worker.onmessage = (event) => {
+ console.log('Main thread received message:', event.data);
+ };
+
+ worker.postMessage({ id: '1', method: 'set', request: { data: 'test' } });
+ worker.postMessage({ id: '2', method: 'remove', request: { data: 'test' } });
+ } catch (error) {
+ console.error('Failed to load worker:', error);
+ }
+}
+
+testWorker();
diff --git a/demos/kit-with-idb/src/lib/utils.ts b/demos/kit-with-idb/src/lib/utils.ts
new file mode 100644
index 00000000..3f7058c0
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/utils.ts
@@ -0,0 +1 @@
+export const nHoursAgo = (hrs: number): number => Math.floor((Date.now() - hrs * 60 * 60 * 1000) / 1000);
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/lib/worker.ts b/demos/kit-with-idb/src/lib/worker.ts
new file mode 100644
index 00000000..f7de429e
--- /dev/null
+++ b/demos/kit-with-idb/src/lib/worker.ts
@@ -0,0 +1,34 @@
+self.addEventListener('message', (e) => {
+ console.log('Worker received message:', e.data);
+ const { id, method, request } = e.data;
+ if (method === 'set') {
+ set(id, request);
+ }
+ if (method === 'remove') {
+ remove(id, request);
+ }
+});
+
+const set = async (id, request) => {
+ console.log('Worker: set', id);
+ try {
+ // Simulate async operation
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ postMessage({ id, method: 'set', success: true });
+ } catch (error) {
+ console.error('Worker: Error setting data', error);
+ postMessage({ id, method: 'set', success: false, error });
+ }
+};
+
+const remove = async (id, request) => {
+ console.log('Worker: remove', id);
+ try {
+ // Simulate async operation
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ postMessage({ id, method: 'remove', success: true });
+ } catch (error) {
+ console.error('Worker: Error removing data', error);
+ postMessage({ id, method: 'remove', success: false, error });
+ }
+};
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/routes/+layout.svelte b/demos/kit-with-idb/src/routes/+layout.svelte
new file mode 100644
index 00000000..fb8c059a
--- /dev/null
+++ b/demos/kit-with-idb/src/routes/+layout.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/routes/+page.svelte b/demos/kit-with-idb/src/routes/+page.svelte
new file mode 100644
index 00000000..241f4369
--- /dev/null
+++ b/demos/kit-with-idb/src/routes/+page.svelte
@@ -0,0 +1,88 @@
+
+
+
+ Total known: {$relays?.length}
+ Primary Monitor: {primaryMonitor}
+ nip11s: {nip11.length}
+ checks: {checks.length}
+
+
+
+
\ No newline at end of file
diff --git a/demos/kit-with-idb/src/routes/relay/[protocol]/[...relay]/+page.svelte b/demos/kit-with-idb/src/routes/relay/[protocol]/[...relay]/+page.svelte
new file mode 100644
index 00000000..12bce3a0
--- /dev/null
+++ b/demos/kit-with-idb/src/routes/relay/[protocol]/[...relay]/+page.svelte
@@ -0,0 +1,28 @@
+
+
+{relay}
+
+primary
+{JSON.stringify(primaryData, null, 2)}
+
+
+secondary
+{JSON.stringify(secondaryDatas, null, 2)}
\ No newline at end of file
diff --git a/packages/nocap/.gitkeep b/demos/kit-with-idb/src/routes/relays/by/country/[countryCode]/+page.svelte
similarity index 100%
rename from packages/nocap/.gitkeep
rename to demos/kit-with-idb/src/routes/relays/by/country/[countryCode]/+page.svelte
diff --git a/packages/publisher/.gitkeep b/demos/kit-with-idb/src/routes/relays/by/follows/[pubkey]/+page.svelte
similarity index 100%
rename from packages/publisher/.gitkeep
rename to demos/kit-with-idb/src/routes/relays/by/follows/[pubkey]/+page.svelte
diff --git a/packages/seed/.gitkeep b/demos/kit-with-idb/src/routes/relays/by/monitor/[monitorPubkey]/+page.svelte
similarity index 100%
rename from packages/seed/.gitkeep
rename to demos/kit-with-idb/src/routes/relays/by/monitor/[monitorPubkey]/+page.svelte
diff --git a/packages/seed/seed.yaml b/demos/kit-with-idb/src/routes/relays/by/nip/[nip]/+page.svelte
similarity index 100%
rename from packages/seed/seed.yaml
rename to demos/kit-with-idb/src/routes/relays/by/nip/[nip]/+page.svelte
diff --git a/packages/status/.gitkeep b/demos/kit-with-idb/src/routes/relays/by/operator-pubkey/[pubkey]/+page.svelte
similarity index 100%
rename from packages/status/.gitkeep
rename to demos/kit-with-idb/src/routes/relays/by/operator-pubkey/[pubkey]/+page.svelte
diff --git a/demos/kit-with-idb/src/routes/relays/by/software/[software]/+page.svelte b/demos/kit-with-idb/src/routes/relays/by/software/[software]/+page.svelte
new file mode 100644
index 00000000..e69de29b
diff --git a/demos/kit-with-idb/src/routes/stream/+page.svelte b/demos/kit-with-idb/src/routes/stream/+page.svelte
new file mode 100644
index 00000000..381dc404
--- /dev/null
+++ b/demos/kit-with-idb/src/routes/stream/+page.svelte
@@ -0,0 +1,113 @@
+
+{relays?.length}
+
+
+{#if relays?.length}
+{#each relays as check}
+
+ {#if check?.url}
+ {check?.pubkey}
{check?.url} ->
+ {/if}
+{/each}
+{/if}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/kit-with-idb/static/favicon.png b/demos/kit-with-idb/static/favicon.png
new file mode 100644
index 00000000..825b9e65
Binary files /dev/null and b/demos/kit-with-idb/static/favicon.png differ
diff --git a/demos/kit-with-idb/svelte.config.js b/demos/kit-with-idb/svelte.config.js
new file mode 100644
index 00000000..10c08e2f
--- /dev/null
+++ b/demos/kit-with-idb/svelte.config.js
@@ -0,0 +1,25 @@
+import adapter from "@sveltejs/adapter-auto";
+import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
+ // for more information about preprocessors
+ preprocess: [vitePreprocess({})],
+
+ kit: {
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
+ // If your environment is not supported or you settled on a specific environment, switch out the adapter.
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
+ adapter: adapter(),
+ alias: {
+ $components: "./src/components",
+ $lib: "./src/lib",
+ $stores: "./src/lib/stores",
+ $utils: "./src/utils",
+ $types: "./src/types",
+ }
+ },
+};
+
+export default config;
diff --git a/demos/kit-with-idb/tailwind.config.cjs b/demos/kit-with-idb/tailwind.config.cjs
new file mode 100644
index 00000000..75fd684f
--- /dev/null
+++ b/demos/kit-with-idb/tailwind.config.cjs
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config}*/
+const config = {
+ content: ["./src/**/*.{html,js,svelte,ts}"],
+
+ theme: {
+ extend: {},
+ },
+
+ plugins: [],
+};
+
+module.exports = config;
diff --git a/demos/kit-with-idb/tsconfig.json b/demos/kit-with-idb/tsconfig.json
new file mode 100644
index 00000000..748b784e
--- /dev/null
+++ b/demos/kit-with-idb/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": false,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/demos/kit-with-idb/vite.config.ts b/demos/kit-with-idb/vite.config.ts
new file mode 100644
index 00000000..d77e48d9
--- /dev/null
+++ b/demos/kit-with-idb/vite.config.ts
@@ -0,0 +1,43 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+import path from 'path';
+import { nodePolyfills } from 'vite-plugin-node-polyfills';
+
+export default defineConfig({
+ plugins: [
+ sveltekit(),
+ nodePolyfills({
+ include: ['crypto']
+ }),
+ ],
+ server: {
+ fs: {
+ allow: [
+ path.resolve(__dirname, 'src'),
+ path.resolve(__dirname, 'src/monitors')
+ ]
+ }
+ },
+ build: {
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ format: 'esm'
+ }
+ }
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ target: 'esnext',
+ supported: { 'top-level-await': true }
+ }
+ },
+ worker: {
+ format: 'es',
+ rollupOptions: {
+ output: {
+ format: 'esm'
+ }
+ }
+ }
+});
\ No newline at end of file
diff --git a/packages/announce/package.json b/internal/announce/package.json
similarity index 100%
rename from packages/announce/package.json
rename to internal/announce/package.json
diff --git a/packages/announce/src/index.test.ts b/internal/announce/src/index.test.ts
similarity index 100%
rename from packages/announce/src/index.test.ts
rename to internal/announce/src/index.test.ts
diff --git a/packages/announce/src/index.ts b/internal/announce/src/index.ts
similarity index 100%
rename from packages/announce/src/index.ts
rename to internal/announce/src/index.ts
diff --git a/packages/announce/src/nostrwatch-publisher.d.ts b/internal/announce/src/nostrwatch-publisher.d.ts
similarity index 100%
rename from packages/announce/src/nostrwatch-publisher.d.ts
rename to internal/announce/src/nostrwatch-publisher.d.ts
diff --git a/packages/announce/tsconfig.json b/internal/announce/tsconfig.json
similarity index 100%
rename from packages/announce/tsconfig.json
rename to internal/announce/tsconfig.json
diff --git a/packages/announce/vitest.config.ts b/internal/announce/vitest.config.ts
similarity index 100%
rename from packages/announce/vitest.config.ts
rename to internal/announce/vitest.config.ts
diff --git a/packages/controlflow/index.js b/internal/controlflow/index.js
similarity index 100%
rename from packages/controlflow/index.js
rename to internal/controlflow/index.js
diff --git a/packages/controlflow/package.json b/internal/controlflow/package.json
similarity index 100%
rename from packages/controlflow/package.json
rename to internal/controlflow/package.json
diff --git a/packages/controlflow/src/queues.js b/internal/controlflow/src/queues.js
similarity index 100%
rename from packages/controlflow/src/queues.js
rename to internal/controlflow/src/queues.js
diff --git a/packages/controlflow/src/retry.js b/internal/controlflow/src/retry.js
similarity index 100%
rename from packages/controlflow/src/retry.js
rename to internal/controlflow/src/retry.js
diff --git a/packages/controlflow/src/scheduler.js b/internal/controlflow/src/scheduler.js
similarity index 100%
rename from packages/controlflow/src/scheduler.js
rename to internal/controlflow/src/scheduler.js
diff --git a/internal/gui-web/.gitkeep b/internal/gui-web/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/gui-web/README.md b/internal/gui-web/README.md
similarity index 100%
rename from packages/gui-web/README.md
rename to internal/gui-web/README.md
diff --git a/internal/kinds/1066/README.md b/internal/kinds/1066/README.md
new file mode 100644
index 00000000..b1e1d4ee
--- /dev/null
+++ b/internal/kinds/1066/README.md
@@ -0,0 +1,26 @@
+# 1066 - Publisher Registration
+
+# Purpose
+For publishers announce intent to publish `30066` and information surrounding their publishing activities. This includes information about their testing limitations, parameters and their environment for determining their results, as well as other metadata.
+
+## Event
+
+```
+{
+ "id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef",
+ "pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
+ "created_at": 1695327657,
+ "kind": 30033,
+ "tags": [
+ ["d", "/relays/online"],
+ ["description", "An index of online relay lists updated periodically"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page1"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page2"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page3"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page4"],
+ ["expiration", "1600000000"]
+ ],
+ "content": "",
+ "sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
+}
+```
\ No newline at end of file
diff --git a/internal/kinds/30066/README.md b/internal/kinds/30066/README.md
new file mode 100644
index 00000000..ca07ebe0
--- /dev/null
+++ b/internal/kinds/30066/README.md
@@ -0,0 +1,3 @@
+# 30066 - Relay Status
+
+```
\ No newline at end of file
diff --git a/internal/kinds/30066/index.js b/internal/kinds/30066/index.js
new file mode 100644
index 00000000..3c49df31
--- /dev/null
+++ b/internal/kinds/30066/index.js
@@ -0,0 +1,85 @@
+import rcache from "./relaydb.js"
+import { chunkArray } from "@nostrwatch/utils"
+
+const HeaderTags = [
+ ['d', '']
+ ['lastpage', ''],
+ ['description', '']
+]
+
+const getEventTpl = () => {
+ return {
+ pubkey: process.env.DAEMON_PUBKEY,
+ kind: 30111,
+ content: "",
+ tags: [],
+ created_at: Math.round(Date.now()/1000)
+ }
+}
+
+const parseATag = ( tag ) => {
+ if(tag[0] !== 'a') return null
+ const [kind, publisher, id] = tag[1].split(':')
+}
+
+const generateRoot = (pagesEvents) => {
+ pagesEvents.forEach(event => {
+ const tag = ['a', `${event.kind}:${event.pubkey}:${event.tags.filter(tag=>tag[0]==='d').map(tag=>tag[1])}`, `${config.publish_to_relay_url}`]]
+ })
+}
+
+const generatePages = (relays) => {
+ if( typeof relays !== 'array' || !relays?.length || typeof relays[0] !== 'string' )
+ throw new Error('invalid relay, must be an array of strings')
+
+ const eventTpl = getEventTpl()
+
+ const events = {
+ pageDir: {},
+ pages: []
+ }
+ const max_tags = config.max_tags_per_page-HeaderTags.length
+
+ // const relays = rcache.relay.get.all('url').map( relay => relay.url )
+ const pages = chunkArray(relays, max_tags)
+ const pdTags = []
+
+ pages.forEach( (page, index) => {
+ //`d`
+ HeaderTags[0][1] = `/${rootSlug}/${pageSlug}/${index+1}`
+ //last page
+ HeaderTags[1][1] = `/relays/found/${pages.length}`
+ const event = {
+ ...eventTpl,
+ tags: page.map( relay => [ 'r', relay ] )
+ }
+ events.pages.push(event)
+ pdTags.push(['a', `30111:${process.env.DAEMON_PUBKEY}:/${rootSlug}/${pageSlug}/${index+1}`, `${config.publish_to_relay_url}`])
+ })
+
+ events.pageDir = {
+ ...eventTpl,
+ tags: [
+ ['d', `/${rootSlug}/found`],
+ ['about', 'Post-filtered aggregate of unique relays in relay lists. Not guaranteed to be free from error, mistyped relays or impossible URLs.'],
+ ...pdTags
+ ]
+ }
+
+ events.root = {
+ ...eventTpl,
+ tags: [
+ ['d', `/${rootSlug}`],
+ ['about', 'Post-filtered aggregate of unique relays in relay lists. Not guaranteed to be free from error, mistyped relays or impossible URLs.'],
+ ...pdTags
+ ]
+ }
+
+ return events
+}
+
+export default(relays) => {
+
+ const events = generateEvents(relays)
+
+}
\ No newline at end of file
diff --git a/internal/kinds/30111/README.md b/internal/kinds/30111/README.md
new file mode 100644
index 00000000..8078beb5
--- /dev/null
+++ b/internal/kinds/30111/README.md
@@ -0,0 +1,96 @@
+# Virtual Directory
+
+A Virtual Directory is a `NIP-51` pattern for producing semi-static, logically ordered, paginated `NIP-51` lists.
+
+## Kind
+
+All Virtual Directories are published to kind `3????` _[30111 maybe?]_ but utilize a patternn composed of one of three types
+
+### Types
+
+- `directory of directories` - This can be seen as the root directory.
+- `directory of pages` - This is a paginated directory. If a single publisher of these indexes published to two different relays with different limitations, the events may be different on each relay. Thus, pulling page types
+- `page of lists` - This can be seen as the root directory.
+
+Types are identified via a `type` event tag and have different limitations.
+
+### Pathing
+Paths for Virtual Directories are set via the `d` tag. Here are some examples
+
+```
+/relays
+/relays/online
+/relays/online/1
+```
+
+In the above example, `/relays` is a directory of directories, `/relays/online` is a directory of pages, and `/relays/online/1` is a page of lists.
+
+
+
+#### Directory of Directories
+
+Not all Virtual Directory
+
+##### Limitations
+- An index of indexes **must** use `a` tags and include a relay where the event can be found. This is because the number of pages found at each of it's list items could differ due to a relay's `max_tags` limitation.
+-
+
+{
+ "id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef",
+ "pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
+ "created_at": 1695327657,
+ "kind": 30033,
+ "tags": [
+ ["d", "/relays"],
+ ["description", "An index of indexes"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|online", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|paid", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|public", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|dead", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|valid", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|clearnet|known", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|tor|...", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|i2p|...", "wss://"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|index|cjdns|...", "wss://"],
+ ["a", "..."],
+ ["expiration", "1600000000"]
+ ],
+ "content": "",
+ "sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
+}
+
+## Index of Pages
+{
+ "id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef",
+ "pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
+ "created_at": 1695327657,
+ "kind": 30033,
+ "tags": [
+ ["d", "/relays/online"],
+ ["description", "An index of online relay lists updated periodically"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page1"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page2"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page3"],
+ ["a", "30033:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:relays|clearnet|online|page4"],
+ ["expiration", "1600000000"]
+ ],
+ "content": "",
+ "sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
+}
+
+## Page of Lists
+{
+ "id": "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e",
+ "pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
+ "created_at": 1695327657,
+ "kind": 30033,
+ "tags": [
+ ["d", "/relays/online/1"],
+ ["description", ""],
+ ["r", "wss://a.relay"],
+ ["a", "30303:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:wss://a.relay", "wss://"],
+ ["expiration", "1600000000"]
+ ],
+ "content": "",
+ "sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
+}
diff --git a/internal/kinds/README.md b/internal/kinds/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/kinds/classes/Publisher.js b/internal/kinds/classes/Publisher.js
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/kinds/classes/Transformer.js b/internal/kinds/classes/Transformer.js
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/logger/index.js b/internal/logger/index.js
new file mode 100644
index 00000000..ea6e6445
--- /dev/null
+++ b/internal/logger/index.js
@@ -0,0 +1,39 @@
+import createLogger from 'logging';
+
+// Wrapper for logging: https://www.npmjs.com/package/logging
+export default class Logger {
+ constructor(name, log_level = "INFO", split_logs = false) {
+ this.logger = createLogger?.default ? createLogger.default(name) : createLogger(name);
+ this.log_level = (log_level || 'info').toUpperCase();
+ this.split_logs = split_logs || false;
+ }
+
+ fatal(message) {
+ if (!['FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG'].includes(this?.log_level)) return;
+ this.logger.error(`FATAL: ${message}`);
+ }
+
+ error(message){
+ this.err(message)
+ }
+
+ err(message) {
+ if (!['ERROR', 'WARN', 'INFO', 'DEBUG'].includes(this?.log_level)) return;
+ this.logger.error(message);
+ }
+
+ warn(message) {
+ if (!['WARN', 'INFO', 'DEBUG'].includes(this?.log_level)) return;
+ this.logger.warn(message);
+ }
+
+ info(message) {
+ if (!['INFO', 'DEBUG'].includes(this?.log_level)) return;
+ this.logger.info(message);
+ }
+
+ debug(message) {
+ if (!['DEBUG'].includes(this?.log_level) && !process.env?.DEBUG) return;
+ this.logger.debug(message);
+ }
+}
diff --git a/packages/logger/package.json b/internal/logger/package.json
similarity index 100%
rename from packages/logger/package.json
rename to internal/logger/package.json
diff --git a/packages/nwcache/defaults.js b/internal/nwcache/defaults.js
similarity index 100%
rename from packages/nwcache/defaults.js
rename to internal/nwcache/defaults.js
diff --git a/packages/nwcache/index.js b/internal/nwcache/index.js
similarity index 100%
rename from packages/nwcache/index.js
rename to internal/nwcache/index.js
diff --git a/packages/nwcache/migrations/find_weird_urls.js b/internal/nwcache/migrations/find_weird_urls.js
similarity index 100%
rename from packages/nwcache/migrations/find_weird_urls.js
rename to internal/nwcache/migrations/find_weird_urls.js
diff --git a/packages/nwcache/migrations/fix_cachetime_kv.js b/internal/nwcache/migrations/fix_cachetime_kv.js
similarity index 100%
rename from packages/nwcache/migrations/fix_cachetime_kv.js
rename to internal/nwcache/migrations/fix_cachetime_kv.js
diff --git a/packages/nwcache/migrations/fix_note_indices.js b/internal/nwcache/migrations/fix_note_indices.js
similarity index 100%
rename from packages/nwcache/migrations/fix_note_indices.js
rename to internal/nwcache/migrations/fix_note_indices.js
diff --git a/packages/nwcache/migrations/fix_relay_records.js b/internal/nwcache/migrations/fix_relay_records.js
similarity index 100%
rename from packages/nwcache/migrations/fix_relay_records.js
rename to internal/nwcache/migrations/fix_relay_records.js
diff --git a/packages/nwcache/migrations/normalize_relay_urls.js b/internal/nwcache/migrations/normalize_relay_urls.js
similarity index 100%
rename from packages/nwcache/migrations/normalize_relay_urls.js
rename to internal/nwcache/migrations/normalize_relay_urls.js
diff --git a/packages/nwcache/migrations/remove_url_hash.js b/internal/nwcache/migrations/remove_url_hash.js
similarity index 100%
rename from packages/nwcache/migrations/remove_url_hash.js
rename to internal/nwcache/migrations/remove_url_hash.js
diff --git a/packages/nwcache/mixins/.template.js b/internal/nwcache/mixins/.template.js
similarity index 100%
rename from packages/nwcache/mixins/.template.js
rename to internal/nwcache/mixins/.template.js
diff --git a/packages/nwcache/mixins/cachetime.js b/internal/nwcache/mixins/cachetime.js
similarity index 100%
rename from packages/nwcache/mixins/cachetime.js
rename to internal/nwcache/mixins/cachetime.js
diff --git a/packages/nwcache/mixins/checks.js b/internal/nwcache/mixins/checks.js
similarity index 100%
rename from packages/nwcache/mixins/checks.js
rename to internal/nwcache/mixins/checks.js
diff --git a/packages/nwcache/mixins/info.js b/internal/nwcache/mixins/info.js
similarity index 100%
rename from packages/nwcache/mixins/info.js
rename to internal/nwcache/mixins/info.js
diff --git a/packages/nwcache/mixins/note.js b/internal/nwcache/mixins/note.js
similarity index 100%
rename from packages/nwcache/mixins/note.js
rename to internal/nwcache/mixins/note.js
diff --git a/packages/nwcache/mixins/relay.js b/internal/nwcache/mixins/relay.js
similarity index 100%
rename from packages/nwcache/mixins/relay.js
rename to internal/nwcache/mixins/relay.js
diff --git a/packages/nwcache/mixins/retry.js b/internal/nwcache/mixins/retry.js
similarity index 100%
rename from packages/nwcache/mixins/retry.js
rename to internal/nwcache/mixins/retry.js
diff --git a/packages/nwcache/mixins/service.js b/internal/nwcache/mixins/service.js
similarity index 100%
rename from packages/nwcache/mixins/service.js
rename to internal/nwcache/mixins/service.js
diff --git a/packages/nwcache/mixins/stat.js b/internal/nwcache/mixins/stat.js
similarity index 100%
rename from packages/nwcache/mixins/stat.js
rename to internal/nwcache/mixins/stat.js
diff --git a/packages/nwcache/package.json b/internal/nwcache/package.json
similarity index 100%
rename from packages/nwcache/package.json
rename to internal/nwcache/package.json
diff --git a/packages/nwcache/schemas.js b/internal/nwcache/schemas.js
similarity index 100%
rename from packages/nwcache/schemas.js
rename to internal/nwcache/schemas.js
diff --git a/packages/nwcache/utils.js b/internal/nwcache/utils.js
similarity index 100%
rename from packages/nwcache/utils.js
rename to internal/nwcache/utils.js
diff --git a/internal/publisher/.gitkeep b/internal/publisher/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/publisher/index.js b/internal/publisher/index.js
similarity index 100%
rename from packages/publisher/index.js
rename to internal/publisher/index.js
diff --git a/packages/publisher/package.json b/internal/publisher/package.json
similarity index 94%
rename from packages/publisher/package.json
rename to internal/publisher/package.json
index b8610281..774f2ca4 100644
--- a/packages/publisher/package.json
+++ b/internal/publisher/package.json
@@ -1,6 +1,6 @@
{
"name": "@nostrwatch/publisher",
- "version": "0.4.2",
+ "version": "0.4.3",
"type": "module",
"description": "Library for publishing nostr.watch relay status and publisher registration events",
"main": "index.js",
diff --git a/packages/publisher/src/Publisher.js b/internal/publisher/src/Publisher.js
similarity index 91%
rename from packages/publisher/src/Publisher.js
rename to internal/publisher/src/Publisher.js
index 9a832a5e..519be07a 100644
--- a/packages/publisher/src/Publisher.js
+++ b/internal/publisher/src/Publisher.js
@@ -76,12 +76,17 @@ export class Publisher {
}
signEvent(event){
- event.id = getEventHash(event)
- event.sig = getSignature(event, process.env.DAEMON_PRIVKEY || "")
- const valid = validateEvent(event) && verifySignature(event)
- if(!valid)
- throw new Error('generateEvent(): event does not validate')
- return event
+ try {
+ event.id = getEventHash(event)
+ event.sig = getSignature(event, process.env.DAEMON_PRIVKEY || "")
+ const valid = validateEvent(event) && verifySignature(event)
+ if(!valid)
+ throw new Error('generateEvent(): event does not validate')
+ return event
+ } catch(e) {
+ this.logger.err(`signEvent(): Error: ${e}`)
+ console.log(event)
+ }
}
signEvents(unsignedEvents){
diff --git a/packages/publisher/src/kinds/Kind0.js b/internal/publisher/src/kinds/Kind0.js
similarity index 100%
rename from packages/publisher/src/kinds/Kind0.js
rename to internal/publisher/src/kinds/Kind0.js
diff --git a/packages/publisher/src/kinds/Kind10002.js b/internal/publisher/src/kinds/Kind10002.js
similarity index 100%
rename from packages/publisher/src/kinds/Kind10002.js
rename to internal/publisher/src/kinds/Kind10002.js
diff --git a/packages/publisher/src/kinds/Kind10166.js b/internal/publisher/src/kinds/Kind10166.js
similarity index 100%
rename from packages/publisher/src/kinds/Kind10166.js
rename to internal/publisher/src/kinds/Kind10166.js
diff --git a/packages/publisher/src/kinds/Kind1066.js b/internal/publisher/src/kinds/Kind1066.js
similarity index 100%
rename from packages/publisher/src/kinds/Kind1066.js
rename to internal/publisher/src/kinds/Kind1066.js
diff --git a/packages/publisher/src/kinds/Kind30066.js b/internal/publisher/src/kinds/Kind30066.js
similarity index 98%
rename from packages/publisher/src/kinds/Kind30066.js
rename to internal/publisher/src/kinds/Kind30066.js
index e3c6d9f6..3718a20c 100644
--- a/packages/publisher/src/kinds/Kind30066.js
+++ b/internal/publisher/src/kinds/Kind30066.js
@@ -84,7 +84,7 @@ export class Kind30066 extends PublisherNocap {
tags.push(['nip11', 'auth_required', 'true'])
else
tags.push(['nip11', 'auth_required', 'false'])
- if(data?.info?.data?.pubkey)
+ if(data?.info?.data?.pubkey && typeof data?.info?.data?.pubkey === 'string')
tags.push(['nip11', 'pubkey', data.info.data.pubkey])
if(data?.info?.data?.contact)
tags.push(['nip11', 'contact', data.info.data.contact])
diff --git a/packages/publisher/src/kinds/Kind30166.js b/internal/publisher/src/kinds/Kind30166.js
similarity index 100%
rename from packages/publisher/src/kinds/Kind30166.js
rename to internal/publisher/src/kinds/Kind30166.js
diff --git a/packages/publisher/tsconfig.json b/internal/publisher/tsconfig.json
similarity index 100%
rename from packages/publisher/tsconfig.json
rename to internal/publisher/tsconfig.json
diff --git a/packages/redis/bullboard.js b/internal/redis/bullboard.js
similarity index 100%
rename from packages/redis/bullboard.js
rename to internal/redis/bullboard.js
diff --git a/packages/redis/docker-compose.yaml b/internal/redis/docker-compose.yaml
similarity index 100%
rename from packages/redis/docker-compose.yaml
rename to internal/redis/docker-compose.yaml
diff --git a/packages/redis/package.json b/internal/redis/package.json
similarity index 100%
rename from packages/redis/package.json
rename to internal/redis/package.json
diff --git a/internal/seed/.gitkeep b/internal/seed/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/seed/package.json b/internal/seed/package.json
similarity index 100%
rename from packages/seed/package.json
rename to internal/seed/package.json
diff --git a/internal/seed/seed.yaml b/internal/seed/seed.yaml
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/seed/src/config.js b/internal/seed/src/config.js
similarity index 100%
rename from packages/seed/src/config.js
rename to internal/seed/src/config.js
diff --git a/packages/seed/src/index.js b/internal/seed/src/index.js
similarity index 100%
rename from packages/seed/src/index.js
rename to internal/seed/src/index.js
diff --git a/packages/seed/src/nwcache.js b/internal/seed/src/nwcache.js
similarity index 100%
rename from packages/seed/src/nwcache.js
rename to internal/seed/src/nwcache.js
diff --git a/internal/status/.gitkeep b/internal/status/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/utils/config.js b/internal/utils/config.js
similarity index 67%
rename from packages/utils/config.js
rename to internal/utils/config.js
index eaba2fc1..217a8622 100644
--- a/packages/utils/config.js
+++ b/internal/utils/config.js
@@ -1,5 +1,3 @@
-import fs from 'fs';
-import fsp from 'fs/promises';
import yaml from 'js-yaml';
let config
@@ -18,19 +16,17 @@ export const extractConfig = async (caller, provider, warn=true) => {
return opts
}
-export const loadConfigSync = function() {
+export const loadConfig = async function() {
+ if(typeof window !== 'undefined') return console.warn('cannot use loadConfig() in the browser.')
+
+ let fsp;
try {
- const configPath = process.env.CONFIG_PATH || './config.yaml';
- if (!configPath) return {};
- const fileContents = fs.readFileSync(configPath, 'utf8');
- return yaml.load(fileContents);
- } catch (e) {
- console.error("loadConfigSync Error: ", e);
- throw new Error('config.yaml not found')
+ const importedFsp = await import('fs/promises');
+ fsp = importedFsp.default || importedFsp;
+ } catch (error) {
+ console.error('Failed to import fs/promises module:', error);
}
-};
-export const loadConfig = async function() {
const handle_error = (e) => {
return new Error('config.yaml not found')
}
diff --git a/packages/utils/env-tools.js b/internal/utils/env-tools.js
similarity index 100%
rename from packages/utils/env-tools.js
rename to internal/utils/env-tools.js
diff --git a/packages/utils/env.js b/internal/utils/env.js
similarity index 100%
rename from packages/utils/env.js
rename to internal/utils/env.js
diff --git a/packages/utils/index.js b/internal/utils/index.js
similarity index 95%
rename from packages/utils/index.js
rename to internal/utils/index.js
index 9a9e4028..384ff30c 100644
--- a/packages/utils/index.js
+++ b/internal/utils/index.js
@@ -3,11 +3,11 @@ import 'dotenv/config'
import network from './network.js'
export { getEnvValue, setEnvValue } from './env-tools.js'
-export { loadConfigSync, loadConfig, extractConfig } from './config.js'
+export { loadConfig, extractConfig } from './config.js'
// export { env } from './env.js'
-let { DAEMON_PUBKEY } = process.env;
+let { DAEMON_PUBKEY } = process?.env || {};
DAEMON_PUBKEY = DAEMON_PUBKEY? DAEMON_PUBKEY : 'WARNING_DAEMON_PUBKEY_UNSET';
export { DAEMON_PUBKEY }
diff --git a/packages/utils/network.js b/internal/utils/network.js
similarity index 100%
rename from packages/utils/network.js
rename to internal/utils/network.js
diff --git a/packages/utils/package.json b/internal/utils/package.json
similarity index 86%
rename from packages/utils/package.json
rename to internal/utils/package.json
index 3ac4d9ae..24699cc7 100644
--- a/packages/utils/package.json
+++ b/internal/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@nostrwatch/utils",
- "version": "0.1.2",
+ "version": "0.1.3",
"type": "module",
"main": "index.js",
"license": "MIT",
diff --git a/lerna.json b/lerna.json
deleted file mode 100644
index 3cee92d4..00000000
--- a/lerna.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "packages": ["packages/*", "packages/nocap/adapters/*"],
- "version": "independent",
- "npmClient": "yarn",
- "useWorkspaces": true,
- "command": {
- "publish": {
- "conventionalCommits": true,
- "allowBranch": "main"
- }
- }
-}
\ No newline at end of file
diff --git a/libraries/idb/.gitignore b/libraries/idb/.gitignore
new file mode 100644
index 00000000..c97fdc24
--- /dev/null
+++ b/libraries/idb/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+.env
+.secrets
+.yalc
+yalc.lock
+dist
+docs
\ No newline at end of file
diff --git a/libraries/idb/package.json b/libraries/idb/package.json
new file mode 100644
index 00000000..00ab7a62
--- /dev/null
+++ b/libraries/idb/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@nostrwatch/idb",
+ "version": "0.0.1",
+ "main": "src/index.ts",
+ "license": "MIT",
+ "type": "module",
+ "dependencies": {
+ "@nostr-dev-kit/ndk": "2.8.2",
+ "@nostrwatch/kit": "0.0.1",
+ "dexie": "4.0.8",
+ "dexie-observable": "4.0.1-beta.13",
+ "dexie-syncable": "4.0.1-beta.13",
+ "json-stringify-deterministic": "1.0.12",
+ "ngeohash": "0.6.3",
+ "object-code": "1.3.3",
+ "object-mapper": "6.2.0",
+ "p-queue": "8.0.1",
+ "vite-plugin-node-polyfills": "0.22.0"
+ },
+ "devDependencies": {
+ "@types/dexie": "1.3.1",
+ "@types/ngeohash": "0.6.8",
+ "@types/node": "20.14.8",
+ "@types/object-mapper": "6.2.2",
+ "@vitest/coverage-v8": "1.6.0",
+ "@vitest/ui": "1.6.0",
+ "http-server": "14.1.1",
+ "rimraf": "5.0.8",
+ "tsdoc": "0.0.4",
+ "tsx": "4.16.2",
+ "typedoc": "0.26.2",
+ "typedoc-plugin-ensure-internal": "https://github.com/deap82/typedoc-plugin-ensure-internal",
+ "typescript": "5.5.2",
+ "vite": "5.3.1",
+ "vite-tsconfig-paths": "4.3.2",
+ "vitest": "1.6.0"
+ },
+ "scripts": {
+ "build": "tsc",
+ "test": "vitest",
+ "test-ui": "vitest --ui --coverage.enabled=true",
+ "start": "node dist/app.js",
+ "dev": "tsc-watch --onSuccess 'node dist/app.js'",
+ "lint": "eslint . --ext .ts",
+ "coverage": "vitest run --reporter=junit --coverage.reporter=json-summary --coverage.enabled=true",
+ "docs": "npx typedoc src/index.ts --out ./docs --ignoreCompilerErrors --plugin typedoc-plugin-ensure-internal",
+ "serve-docs": "http-server ./docs",
+ "clean": "rimraf dist"
+ },
+ "files": [
+ "README.md",
+ "dist",
+ "src",
+ "tsconfig.json",
+ ".yalc"
+ ]
+}
diff --git a/libraries/idb/src/index.ts b/libraries/idb/src/index.ts
new file mode 100644
index 00000000..b1714176
--- /dev/null
+++ b/libraries/idb/src/index.ts
@@ -0,0 +1,5 @@
+export * from "./relays"
+export * from "./monitors"
+export * from "./utils"
+export * from "./types"
+export * from 'dexie'
\ No newline at end of file
diff --git a/libraries/idb/src/monitors/db.ts b/libraries/idb/src/monitors/db.ts
new file mode 100644
index 00000000..977a7fd0
--- /dev/null
+++ b/libraries/idb/src/monitors/db.ts
@@ -0,0 +1,32 @@
+import Dexie, { Table, Transaction } from 'dexie';
+import { IMonitor } from './tables'
+
+export class MonitorDb extends Dexie {
+ monitors!: Table;
+
+ constructor() {
+ super('MonitorDb');
+ this.version(1).stores({
+ monitors: '&monitorPubkey, *checks, *kinds, geohash, *geocode, isActive, last_active',
+ });
+ this.hooks()
+ }
+
+ hooks(){
+ // this.monitors.hook("deleting", async (primKey: string | undefined, obj: IMonitor, transaction: Transaction) => {
+ // const { monitorPubkey } = obj;
+ // await Promise.all([
+ // RelayDb.relayChecks.where({monitorPubkey}).delete(),
+ // RelayDb.nip11s.where({monitorPubkey}).delete(),
+ // RelayDb.ssls.where({monitorPubkey}).delete(),
+ // RelayDb.dnses.where({monitorPubkey}).delete()
+ // ])
+ // });
+ }
+}
+
+const db: MonitorDb = new MonitorDb();
+
+db.open()
+
+export default db;
diff --git a/libraries/idb/src/monitors/index.ts b/libraries/idb/src/monitors/index.ts
new file mode 100644
index 00000000..9cf1ed02
--- /dev/null
+++ b/libraries/idb/src/monitors/index.ts
@@ -0,0 +1,8 @@
+export * from './tables'
+export { MonitorDb } from './db'
+
+import transform from './transform'
+export const MonitorTransform = transform
+
+import db from './db'
+export const monitorDb = db
\ No newline at end of file
diff --git a/libraries/idb/src/monitors/routines.ts b/libraries/idb/src/monitors/routines.ts
new file mode 100644
index 00000000..4187148e
--- /dev/null
+++ b/libraries/idb/src/monitors/routines.ts
@@ -0,0 +1,33 @@
+import monitorDb from "./db";
+import { eventsPurgeMonitors } from "src/relays/routines";
+
+export const monitorsPurgeLastSeen = (thresholdMillis: number = 3600000) => {
+ const threshold = thresholdMillis / 1000;
+ const now = Date.now() / 1000;
+ const cutoff = now - threshold;
+ const monitors = monitorDb.monitors;
+ const count = monitors.count();
+ console.log("Purging monitors last seen before", cutoff, "(", count, "monitors)");
+ monitors.where("lastSeen").below(cutoff).delete();
+}
+
+export const monitorsPurgePubkey = async (monitorPubkeys: string | string[]) => {
+ const monitors = monitorDb.monitors;
+ if(!monitors) return
+ if(typeof monitorPubkeys === "string") {
+ monitorPubkeys = [monitorPubkeys];
+ }
+ const count = monitors.count();
+ console.log("Purging monitors for monitors", monitorPubkeys, "(", count, "monitors)");
+ await monitors.where("monitorPubkey").anyOf(monitorPubkeys).delete();
+ await eventsPurgeMonitors(monitorPubkeys)
+}
+
+
+export const monitorsPurgeAll = () => {
+ const monitors = monitorDb.monitors;
+ if(!monitors) return
+ const count = monitors.count();
+ console.log("Purging all monitors", "(", count, "monitors)");
+ monitors.clear();
+}
\ No newline at end of file
diff --git a/libraries/idb/src/monitors/tables.ts b/libraries/idb/src/monitors/tables.ts
new file mode 100644
index 00000000..75b3525d
--- /dev/null
+++ b/libraries/idb/src/monitors/tables.ts
@@ -0,0 +1,14 @@
+import type { NostrEvent } from '@nostr-dev-kit/ndk';
+
+export interface IMonitor {
+ monitorPubkey: string;
+ created_at: number;
+ frequency: number;
+ checks?: string[] | undefined;
+ kinds?: number[] | undefined;
+ geohash?: string | undefined;
+ geocode?: (number | string)[] | undefined
+ isActive?: number | undefined;
+ lastActive?: number;
+ event: NostrEvent;
+}
diff --git a/libraries/idb/src/monitors/transform.ts b/libraries/idb/src/monitors/transform.ts
new file mode 100644
index 00000000..56ab8080
--- /dev/null
+++ b/libraries/idb/src/monitors/transform.ts
@@ -0,0 +1,43 @@
+import { NostrEvent, NDKRelayMonitor } from '@nostr-dev-kit/ndk';
+import { IMonitor } from './tables';
+
+export type MonitorCacheData = {
+ monitor: IMonitor,
+}
+
+const transform = ( relayMonitor: NDKRelayMonitor ): MonitorCacheData => {
+
+ const { pubkey:monitorPubkey, checks, kinds, geohash, active:isActive, created_at:lastActive } = relayMonitor;
+ const created_at: number = relayMonitor.created_at as number;
+
+ console.log('transform:active?', isActive)
+
+ const frequency = relayMonitor.frequency;
+
+ let geocode: string[] = []
+
+ if(relayMonitor.countryCode) geocode = relayMonitor.countryCode
+ if(relayMonitor.regionCode) geocode = [ ...geocode, ...relayMonitor.regionCode ]
+
+ const event = relayMonitor.rawEvent() as NostrEvent;
+
+ const monitor = {
+ monitorPubkey,
+ frequency,
+ created_at,
+ checks,
+ kinds,
+ geohash,
+ geocode,
+ isActive: isActive? 1: 0,
+ event
+ }
+
+ console.log('transformed', monitorPubkey, monitor)
+
+ return {
+ monitor
+ }
+}
+
+export default transform
\ No newline at end of file
diff --git a/libraries/idb/src/monitors/worker.ts b/libraries/idb/src/monitors/worker.ts
new file mode 100644
index 00000000..015daf21
--- /dev/null
+++ b/libraries/idb/src/monitors/worker.ts
@@ -0,0 +1,21 @@
+import db from './db'
+
+self.onmessage = async (event: MessageEvent) => {
+ const {method, data} = event.data
+
+}
+
+const addToMonitorDb = async (data: Record) => {
+ try {
+ await Promise.all([
+ db.monitors.put(data.monitor),
+ ]);
+ } catch (err) {
+ console.error('Failed to add mapped objects to DB:', err);
+ }
+}
+
+// export default async ( dirtyData: MonitorDataDirty) => {
+// const transformedData = transform(dirtyData)
+// await addToMonitorDb( transformedData )
+// }
\ No newline at end of file
diff --git a/libraries/idb/src/relays/db.test.ts b/libraries/idb/src/relays/db.test.ts
new file mode 100644
index 00000000..0215cc3c
--- /dev/null
+++ b/libraries/idb/src/relays/db.test.ts
@@ -0,0 +1,142 @@
+import Dexie, { Table } from 'dexie';
+
+Dexie.debug = true;
+
+// Define interfaces for your data
+export interface IEvent {
+ nid: string;
+ rid: number;
+ monitorPubkey: string;
+ relay?: string | null;
+ event: any; // Assuming NostrEvent is correctly defined elsewhere
+ createdAt: number | null;
+}
+
+// Define other interfaces as needed
+export interface IRelay {
+ rid: string;
+ lastSeen: number;
+ score: number;
+}
+
+export interface ICheck {
+ nid: string;
+ rid: number;
+ monitorPubkey: string;
+ operatorPubkey: string;
+ network: string;
+ category: string;
+ open: number;
+ read: number;
+ write: number;
+ geohash: string;
+ geocode: string[];
+ supportedNips: string[];
+ paymentRequired: boolean;
+ authRequired: boolean;
+ software: string;
+ version: string;
+ validTo: number;
+ issuer: string;
+ ipv4: string;
+ ipv6: string;
+ isp: string;
+ createdAt: number;
+}
+
+export interface INip11 {
+ rid: number;
+ monitorPubkey: string;
+ hash: string;
+ nid: string;
+ json: string;
+}
+
+export interface IGlobalRtt {
+ rid: number;
+ monitorPubkeys: string[];
+ operatorPubkey: string;
+ avgAll: number;
+ avgOpen: number;
+ avgRead: number;
+ avgWrite: number;
+ createdAt: number;
+}
+
+// Define the database class
+export class RelayDb extends Dexie {
+ relays!: Table;
+ checks!: Table;
+ pastChecks!: Table;
+ nip11s!: Table;
+ events!: Table;
+ globalRtts!: Table;
+
+ static indices: Record = {
+ relays: `&rid, lastSeen, score`,
+ checks: `&nid, [rid+monitorPubkey], monitorPubkey, operatorPubkey, network, category, open, read, write, geohash, *geocode, *supportedNips, paymentRequired, authRequired, software, version, validTo, issuer, ipv4, ipv6, isp, createdAt`,
+ pastChecks: `&nid, [rid+monitorPubkey], monitorPubkey, operatorPubkey, createdAt`,
+ nip11s: `[rid+monitorPubkey], monitorPubkey, hash, nid, json`,
+ events: `&nid, monitorPubkey, createdAt`,
+ geocodes: `&id++, [type+format+type+length], [type+format+type],[format+type], code, type, format, length`,
+ globalRtts: `&rid, *monitorPubkeys, operatorPubkey, avgAll, avgOpen, avgRead, avgWrite, createdAt`
+ };
+
+ constructor() {
+ super("RelayDb");
+
+ // Enhanced error handling during initialization
+ try {
+ this.version(1).stores(RelayDb.indices);
+ } catch (error) {
+ console.error('Dexie Initialization Error:', error);
+ alert('Dexie Initialization Error: ' + error.message);
+ }
+
+ // Add custom error handling
+ this.on('error', (error) => {
+ console.error('Dexie Error:', error);
+ alert('Dexie Error: ' + error.message);
+ });
+ }
+}
+
+// Initialize the database
+const db = new RelayDb();
+
+// Function to add an event and catch detailed errors
+async function addExampleEvent(db: RelayDb) {
+ const exampleEvent: IEvent = {
+ nid: '57e014a4fc9cd23971aa6e5c048fa9314148c63eb072a14753accb89c16839d8',
+ rid: 825000170,
+ monitorPubkey: '9b85d54cc4bc886d60782f80d676e41bc637ed3ecc73d2bb5aabadc499d6a340',
+ relay: 'wss://nproxy.kristapsk.lv/',
+ event: {
+ created_at: 1720442141,
+ content: '',
+ tags: [
+ ['d', 'wss://nproxy.kristapsk.lv/'],
+ ['other', 'network', 'clearnet'],
+ ['rtt', 'open', '1018'],
+ ['rtt', 'read', '2702']
+ ],
+ kind: 30066,
+ pubkey: '9b85d54cc4bc886d60782f80d676e41bc637ed3ecc73d2bb5aabadc499d6a340',
+ id: '57e014a4fc9cd23971aa6e5c048fa9314148c63eb072a14753accb89c16839d8',
+ sig: 'dc676a1702c8edf49c2bae4e9d396a5b2a2a2b13c5baa45aecfd9c4a795066c27c36204a07ca8037aea10969158a6d8a2b22c6492afaa1815cd0d4aa0f6eb561'
+ },
+ createdAt: 1720442141
+ };
+
+ try {
+ await db.transaction('rw', db.events, async () => {
+ await db.events.add(exampleEvent);
+ });
+ } catch (error) {
+ console.error('Transaction Error:', error);
+ alert('Transaction Error: ' + error.message);
+ }
+}
+
+// Example usage
+addExampleEvent(db);
diff --git a/libraries/idb/src/relays/db.ts b/libraries/idb/src/relays/db.ts
new file mode 100644
index 00000000..d9fb5722
--- /dev/null
+++ b/libraries/idb/src/relays/db.ts
@@ -0,0 +1,299 @@
+import Dexie, { Table, Middleware, DBCoreTable, DBCoreMutateRequest, DBCoreGetRequest, DBCoreMutateResponse } from 'dexie';
+import { IRelay, ICheck, IEvent, IGlobalRtt, INip11, ISsl } from './tables';
+import nostrEventToRelayDb from './transform';
+import { NDKRelayMeta } from '@nostr-dev-kit/ndk';
+import { IGeoCode } from 'src/shared/tables';
+
+const devnull = () => {}
+
+import * as fastq from "fastq";
+import type { queueAsPromised } from "fastq";
+
+const queue: queueAsPromised<()=>{}> = fastq.promise(worker, 1)
+
+async function worker (cb: () => {}): Promise {
+ cb()
+ // console.log('added from worker')
+}
+
+export class RelayDb extends Dexie {
+ static NAME: string = 'RelayDb'
+ readonly VERSION: number = 1;
+
+ relays!: Table;
+ checks!: Table;
+ pastChecks!: Table;
+ nip11s!: Table;
+ geocodes!: Table;
+ globalRtts!: Table;
+ ssls!: Table;
+ events!: Table;
+
+ static indices: Record = {
+ relays: `&relay, lastSeen`,
+
+ checks: `[relay+monitorPubkey], [software+version],
+ nid,
+ relay, monitorPubkey, operatorPubkey,
+ network, category,
+ open, geohash, paymentRequired, authRequired,
+ validTo,
+ ipv4, isp,
+ *geocode, *supportedNips,
+ createdAt`,
+
+ nip11s: `[relay+monitorPubkey], monitorPubkey, hash, json`,
+
+ events: `nid, relay, monitorPubkey, kind, createdAt`,
+
+ geocodes: `code, [type+format+type+length], [type+format+type],[type+format], type, format, length`,
+
+ ssls: `relay, monitorPubkey, hash, nid`
+
+ // globalRtts: `&relay, *monitorPubkeys, operatorPubkey,
+ // avgAll, avgOpen, avgRead, avgWrite,
+ // createdAt`
+ };
+
+ constructor() {
+ super(RelayDb.NAME);
+
+ try {
+ this.version(this.VERSION).stores(RelayDb.indices);
+ } catch (error) {
+ console.error("Error setting up the database schema:", error);
+ Object.entries(RelayDb.indices).forEach(([table, index]) => {
+ try {
+ this.version(this.VERSION).stores({ [table]: index });
+ } catch (innerError) {
+ console.error(`Error setting up index for table "${table}" with index "${index}":`, innerError);
+ }
+ });
+ }
+
+ this.open().then(() => {
+ console.log('Database opened successfully');
+ console.log('Tables:', this.tables.map(table => table.name));
+ }).catch((err) => {
+ console.error("Failed to open db: " + err.stack || err);
+ });
+
+ // this.use({
+ // stack: 'dbcore',
+ // create: (downlevelDatabase) => {
+ // return {
+ // ...downlevelDatabase,
+ // table: (name) => {
+ // const downlevelTable = downlevelDatabase.table(name);
+ // return {
+ // ...downlevelTable,
+ // mutate: async (req) => {
+ // //console.log(`Mutate called on table ${name} with request:`, req);
+ // // req = await this.beforeMutate(name, req)
+ // const result = await downlevelTable.mutate(req);
+ // //console.log(`Mutation result for table ${name}:`, result);
+ // await this.afterMutate(name, req);
+ // return result;
+ // }
+ // };
+ // }
+ // };
+ // }
+ // });
+ }
+
+ static defaults(): { [K in keyof T]: T[K] | null } {
+ const defaultObject = {} as { [K in keyof T]: T[K] | null };
+ Object.keys(defaultObject).forEach(key => {
+ defaultObject[key as keyof T] = null as any;
+ });
+ return defaultObject;
+ }
+
+ async beforeMutate(tableName: string, req: DBCoreMutateRequest): Promise {
+ // console.log(`beforeMutate called for table ${tableName} with request:`, req);
+ try {
+ if (tableName === 'checks') {
+ req = await this.handleChecksBeforeMutate(req).catch(console.error);
+ }
+ if (tableName === 'relays') {
+ req = await this.handleRelaysBeforeMutate(req).catch(console.error);
+ }
+ } catch (error) {
+ console.error(`Error in beforeMutate for table ${tableName}:`, error);
+ }
+ return req
+ }
+
+ async afterMutate(tableName: string, req: DBCoreMutateRequest) {
+ // console.log(`afterMutate called for table ${tableName} with request:`, req);
+ try {
+ if (tableName === 'events') {
+ // console.log('Handling events ');
+ await this.handleEventsAfterMutate(req);
+ }
+ if (tableName === 'checks') {
+ await this.handleChecksAfterMutate(req);
+ }
+ if (tableName === 'relays') {
+ await this.handleRelaysAfterMutate(req);
+ }
+ } catch (error) {
+ console.error(`Error in afterMutate for table ${tableName}:`, error);
+ }
+ }
+
+
+ async handleEventsAfterMutate(req: DBCoreMutateRequest) {
+ if (req.type === 'add' || req.type === 'put') {
+ for (const event of req.values) {
+ const { event: nostrEvent } = event;
+ const { check, relay, nip11, geocodes, ssl } = await nostrEventToRelayDb(nostrEvent);
+ if (relay) queue.push( (): void => { this.relays.add(relay).catch( () => console.warn(`${relay.relay} already exists`)) })
+ if (check) queue.push( (): void => { this.checks.put(check) })
+ // Dexie.waitFor(async () => {
+ // const promises = [];
+
+ // // if (nip11) promises.push(this.nip11s.put(nip11));
+ // // if (geocodes?.length) promises.push(this.geocodes.bulkPut(geocodes).catch(devnull));
+ // // if (ssl) promises.push(this.ssls.put(ssl));
+ // await Promise.all(promises);
+ // });
+ }
+ }
+
+ if (req.type === 'delete') {
+ for (const key of req.keys) {
+ const oldEvent = await this.events.get(key);
+ if (!oldEvent) return;
+ const { event: nostrEvent } = oldEvent;
+ const { check } = await nostrEventToRelayDb(nostrEvent);
+
+ Dexie.waitFor(async () => {
+ const promises = [];
+ if (check) {
+ const { nid } = check;
+ promises.push(this.checks.delete(nid));
+ promises.push(this.pastChecks.delete(nid));
+ }
+ await Promise.all(promises);
+ });
+ }
+ }
+ }
+ async handleRelaysBeforeMutate(req: DBCoreMutateRequest): Promise {
+ // if (req.type === 'add' || req.type === 'put') {
+ // for (const relay of req.values) {
+ // const { relay: relayKey, lastSeen } = relay;
+ // const transaction = Dexie.currentTransaction;
+ // if (transaction) {
+ // const existingRelay = await this.relays.get(relayKey);
+ // if (existingRelay && existingRelay.lastSeen < lastSeen) {
+ // req.values = req.values.map(value => ({
+ // ...value,
+ // createdAt: lastSeen
+ // }));
+ // }
+ // } else {
+ // console.error('No current transaction found for handleRelaysAfterMutate');
+ // }
+ // }
+ // }
+ return req
+ }
+
+ async handleRelaysAfterMutate(req: DBCoreMutateRequest) {
+ // if (req.type === 'add' || req.type === 'put') {
+ // for (const relay of req.values) {
+ // const { relay: relayKey, lastSeen } = relay;
+ // const transaction = Dexie.currentTransaction;
+ // if (transaction) {
+ // transaction.on('complete', async () => {
+ // const existingRelay = await this.relays.get(relayKey);
+ // if (existingRelay && existingRelay.lastSeen < lastSeen) {
+ // await this.relays.where({ relay: relayKey }).modify({ lastSeen });
+ // }
+ // });
+ // } else {
+ // console.error('No current transaction found for handleRelaysAfterMutate');
+ // }
+ // }
+ // }
+
+ if (req.type === 'delete') {
+ const transaction = Dexie.currentTransaction;
+
+ if (transaction) {
+ transaction.on('complete', async () => {
+ for (const key of req.keys) {
+ const promises = [];
+ promises.push(this.checks.where({ relay: key }).delete());
+ promises.push(this.pastChecks.where({ relay: key }).delete());
+ promises.push(this.globalRtts.where({ relay: key }).delete());
+ promises.push(this.events.where({ relay: key }).delete());
+ await Promise.all(promises);
+ }
+ });
+ } else {
+ console.error('No current transaction found for handleRelaysAfterMutate delete');
+ }
+ }
+ }
+
+ async handleChecksBeforeMutate(req: DBCoreMutateRequest): Promise{
+ // if (req.type === 'add' || req.type === 'put') {
+ // for (const check of req.values) {
+ // const { createdAt: lastSeen, relay, monitorPubkey, nid } = check;
+ // const checksToProcess = await this.checks
+ // .where({ relay, monitorPubkey })
+ // .and(existingCheck => existingCheck.nid !== check.nid)
+ // .toArray();
+ // if(!checksToProcess.length) return req;
+ // await Promise.all(checksToProcess.map(async (existingCheck) => {
+ // await this.checks.delete(existingCheck.nid)
+ // })
+ // );
+ // }
+ // }
+ return req
+ }
+
+ async handleChecksAfterMutate(req: DBCoreMutateRequest) {
+ // if (req.type === 'add' || req.type === 'put') {
+ // for (const check of req.values) {
+ // const transaction = Dexie.currentTransaction;
+ // const { createdAt: lastSeen, relay, monitorPubkey, nid } = check;
+ // if (transaction) {
+ // transaction.on('complete', async () => {
+ // const lastSeenExisting = (await this.relays.get(relay))?.lastSeen
+ // if(lastSeenExisting && (lastSeen > lastSeenExisting) ) await this.relays.where({ relay }).modify({ lastSeen });
+ // });
+ // } else {
+ // console.error('No current transaction found for handleChecksAfterMutate');
+ // }
+ // }
+ // }
+
+ if (req.type === 'delete') {
+ const transaction = Dexie.currentTransaction;
+
+ if (transaction) {
+ transaction.on('complete', async () => {
+ for (const key of req.keys) {
+ const oldCheck = await this.checks.get(key);
+ if (!oldCheck) return;
+ await this.pastChecks.put(oldCheck);
+ }
+ });
+ } else {
+ console.error('No current transaction found for handleChecksAfterMutate delete');
+ }
+ }
+ }
+}
+
+const db: RelayDb = new RelayDb();
+
+db.open()
+
+export default db;
diff --git a/libraries/idb/src/relays/getters.ts b/libraries/idb/src/relays/getters.ts
new file mode 100644
index 00000000..78c9c64c
--- /dev/null
+++ b/libraries/idb/src/relays/getters.ts
@@ -0,0 +1,144 @@
+import db from './db'
+import geohash from 'ngeohash';
+import { normalizeUrl } from '../utils';
+import { NDKRelayMeta } from '@nostr-dev-kit/ndk';
+import { allOf } from '../utils';
+import { ICheck, IRelay } from './tables';
+
+export const allRelays = async () => {
+ return db.relays.toArray();
+};
+
+export const getRelaysOnly = async () => {
+ return (await db.relays.toArray())?.map( (relay: IRelay) => relay.relay);
+};
+
+export const supportsNips = async (nips: number[]) => {
+ const promises = [];
+ (await allOf(db.checks, 'supportedNips', nips)).forEach( (c: ICheck) => {
+ promises.push(async () => {
+ const event = await db.events.get(c.nid)
+ return new NDKRelayMeta(undefined, event?.event)
+ })
+ })
+};
+
+export const nip11 = async (relayUrl: string) => {
+ relayUrl = normalizeUrl(relayUrl)
+ try {
+ return (await db.nip11s.get({ relay: relayUrl }))?.json
+ } catch(e){
+ console.error(e)
+ return new Object()
+ }
+};
+
+export const getRelayMeta = async (relay: string): Promise => {
+ relay = normalizeUrl(relay);
+ const nostrEvent = (await db.events.get({ relay }))?.event
+ return new NDKRelayMeta(undefined, nostrEvent)
+};
+
+export const getPaymentRequired = async () => {
+ return db.checks.where('paymentRequired').equals(1).toArray();
+};
+
+export const getAuthRequired = async () => {
+ return db.checks.where('authRequired').equals(1).toArray();
+};
+
+export const getNoPaymentRequired = async () => {
+ return db.checks.where('paymentRequired').equals(0).toArray();
+};
+
+export const getNoAuthRequired = async () => {
+ return db.checks.where('authRequired').equals(0).toArray();
+};
+
+export const getUsesSoftware = async (software: string) => {
+ return db.checks.where('software').equalsIgnoreCase(software).toArray();
+};
+
+export const getUsesSoftwareVersion = async (software: string, version: string) => {
+ return db.checks.where({software, version}).toArray();
+};
+
+export const getOpenRttBetween = async (min: number, max: number) => {
+ return db.checks.where('open').between(min, max).toArray();
+};
+
+export const getReadRttBetween = async (min: number, max: number) => {
+ return db.checks.where('read').between(min, max).toArray();
+};
+
+export const getWriteRttBetween = async (min: number, max: number) => {
+ return db.checks.where('write').between(min, max).toArray();
+};
+
+export const getOpenRttLowerThan = async (maxOpen: number) => {
+ return db.checks.where('open').below(maxOpen).toArray();
+};
+
+export const getReadRttLowerThan = async (maxRead: number) => {
+ return db.checks.where('read').below(maxRead).toArray();
+};
+
+export const getWriteRttLowerThan = async (maxWrite: number) => {
+ return db.checks.where('write').below(maxWrite).toArray();
+};
+
+export const getOpenRttGreaterThan = async (minOpen: number) => {
+ return db.checks.where('open').above(minOpen).toArray();
+};
+
+export const getReadRttGreaterThan = async (minRead: number) => {
+ return db.checks.where('read').above(minRead).toArray();
+};
+
+export const getWriteRttGreaterThan = async (minWrite: number) => {
+ return db.checks.where('write').above(minWrite).toArray();
+};
+
+export const sslIsValid = async () => {
+ const now = Date.now();
+ return db.checks.where('valid_to').above(now).toArray();
+};
+
+export const sslIsInvalid = async () => {
+ const now = Date.now();
+ return db.checks.where('valid_to').below(now).toArray();
+};
+
+export const noSsl = async () => {
+ return db.checks.where('valid_to').equals(0).toArray();
+};
+
+export const getRelayIpIs = async (ipAddress: string) => {
+ return (await db.checks.where('ipv4').equals(ipAddress).or('ipv6').equals(ipAddress).toArray());
+};
+
+export const getRelayGeohashIs = async (geohash: string) => {
+ return db.checks.where('geohash').equals(geohash).toArray();
+};
+
+export const getRelayCountryCodeIs = async (countryCode: string) => {
+ return db.checks.where('geocode').equals(countryCode).toArray();
+};
+
+export const getRelayIsWithin = async (gh: string, distance: number, precision = 9, unit = 'km') => {
+ if(unit === 'miles') distance = distance * 1.60934;
+ const { latitude, longitude } = geohash.decode(gh);
+ const earthRadiusKm = 6371;
+ const distanceLatKm = distance / earthRadiusKm;
+ const distanceLonKm = distance / (earthRadiusKm * Math.cos(Math.PI * latitude / 180));
+
+ // Calculate bounds
+ const minLat = latitude - distanceLatKm * (180 / Math.PI);
+ const maxLat = latitude + distanceLatKm * (180 / Math.PI);
+ const minLon = longitude - distanceLonKm * (180 / Math.PI);
+ const maxLon = longitude + distanceLonKm * (180 / Math.PI);
+
+ const boundingBoxGeohashes = geohash.bboxes(minLat, minLon, maxLat, maxLon, precision);
+
+ return db.checks.where('geohash').anyOf(boundingBoxGeohashes).toArray();
+};
\ No newline at end of file
diff --git a/libraries/idb/src/relays/index.ts b/libraries/idb/src/relays/index.ts
new file mode 100644
index 00000000..d832f42f
--- /dev/null
+++ b/libraries/idb/src/relays/index.ts
@@ -0,0 +1,11 @@
+export * from './getters'
+export * from './tables'
+export * from './transform'
+export * from './routines'
+export { RelayDb } from './db'
+
+import transform from './transform'
+export const RelayTransform = transform
+
+import db from './db'
+export const relayDb = db
\ No newline at end of file
diff --git a/libraries/idb/src/relays/routines.ts b/libraries/idb/src/relays/routines.ts
new file mode 100644
index 00000000..d2ebbcba
--- /dev/null
+++ b/libraries/idb/src/relays/routines.ts
@@ -0,0 +1,53 @@
+import relayDb from "./db";
+// import type { RelayDb } from "./db";
+
+const events = relayDb.events;
+
+export const eventsPurgeAge = async (thresholdMillis: number = 1000*60*60*24) => {
+ const threshold = thresholdMillis / 1000;
+ const now = Date.now() / 1000;
+ const cutoff = now - threshold;
+ const count = await events.count();
+ console.log("Purging events older than", cutoff, "(", count, "events)");
+ events.where("createdAt").below(cutoff).delete();
+}
+
+export const eventsPurgeCount = async (maximumEvents: number = 1000) => {
+ if(!events) return
+ const count = await events.count();
+ if(count > maximumEvents) {
+ const eventsToPurge = events.orderBy("createdAt").reverse().offset(maximumEvents);
+ const cutoff = count - maximumEvents;
+ console.log("Purging events older than", cutoff, "(", count, "events)");
+ await eventsToPurge.delete();
+ }
+}
+
+export const eventsPurgeMonitors = async (monitorPubkeys: string | string[]) => {
+ if(!events) return
+ if(typeof monitorPubkeys === "string") {
+ monitorPubkeys = [monitorPubkeys];
+ }
+ const eventsToPurge = events.where("monitorPubkey").anyOf(monitorPubkeys)
+ const count = events.count();
+ console.log("Purging events for monitors", monitorPubkeys, "(", count, "events)");
+ await eventsToPurge.delete();
+}
+
+export const eventsPurgeRelays = async (relays: string | string[]) => {
+ if(!events) return
+ if(typeof relays === "string") {
+ relays = [relays];
+ }
+ const eventsToPurge = events.where("relay").anyOf(relays)
+ const count = await eventsToPurge.count();
+ console.log("Purging events for relays", relays, "(", count, "events)");
+ eventsToPurge.delete();
+}
+
+export const eventsPurgeAll = async () => {
+ if(!events) return
+ const count = await events.count();
+ console.log("Purging all events", "(", count, "events)");
+ await events.clear();
+}
\ No newline at end of file
diff --git a/libraries/idb/src/relays/setters.ts b/libraries/idb/src/relays/setters.ts
new file mode 100644
index 00000000..ff8a0cf7
--- /dev/null
+++ b/libraries/idb/src/relays/setters.ts
@@ -0,0 +1,18 @@
+import { NostrEvent, NDKRelayMeta } from "@nostr-dev-kit/ndk";
+import $RelayDb from "./db";
+
+export const addRelayMetaEvent = async (event: NDKRelayMeta) => {
+ if(!event?.id) return false
+ return addNostrEvent(event.rawEvent() as NostrEvent)
+};
+
+export const addNostrEvent = async (event: NostrEvent) => {
+ if(!event?.id) return false
+ await $RelayDb.events.add({
+ nid: event.id as string,
+ monitorPubkey: event.pubkey,
+ relay: event.tags.find( t => t[0] === 'd')?.[1],
+ createdAt: event.created_at,
+ event
+ });
+};
\ No newline at end of file
diff --git a/libraries/idb/src/relays/tables.ts b/libraries/idb/src/relays/tables.ts
new file mode 100644
index 00000000..2100ef69
--- /dev/null
+++ b/libraries/idb/src/relays/tables.ts
@@ -0,0 +1,78 @@
+import type { NostrEvent } from "@nostr-dev-kit/ndk";
+
+export interface IEvent {
+ nid: string;
+ monitorPubkey: string;
+ kind: number;
+ relay?: string | null;
+ event: NostrEvent;
+ createdAt: number | null;
+}
+
+export interface IRelay {
+ relay: string;
+ lastSeen: number;
+ network: string;
+ score?: number | null;
+}
+
+export interface ICheck {
+ nid: string;
+ createdAt: number;
+ relay: string;
+ monitorPubkey: string;
+ operatorPubkey?: string | null;
+ network?: string | null;
+ open?: number | null;
+ read?: number | null;
+ write?: number | null;
+ geohash?: string | null;
+ geocode?: (number | string)[] | null;
+ validTo?: number | null;
+ issuer?: string | null;
+ ipv4?: string | null;
+ ipv6?: string | null;
+ isp?: string | null;
+ supportedNips?: number[] | null;
+ paymentRequired?: number | null;
+ authRequired?: number | null;
+ software?: string | null;
+ version?: string | null;
+}
+
+export interface INip11 {
+ createdAt: number;
+ relay: string;
+ hash: string | null;
+ json: Record | null;
+ monitorPubkey?: string;
+ nid?: string | null; //might not be set if populated outside of NIP-66
+}
+
+export interface IPastChecks {
+ nid: string;
+ relay: string;
+ createdAt: number;
+ monitorPubkey: string;
+ operatorPubkey: string;
+}
+
+export interface IGlobalRtt {
+ relay: string;
+ createdAt: number;
+ monitorPubkeys: string[];
+ operatorPubkey: string;
+ avgAll: number;
+ avgOpen: number;
+ avgRead: number;
+ avgWrite: number;
+}
+
+export interface ISsl {
+ nid: string;
+ relay: string;
+ createdAt: number;
+ monitorPubkey: string;
+ hash: string;
+ cert: string;
+}
\ No newline at end of file
diff --git a/libraries/idb/src/relays/transform.ts b/libraries/idb/src/relays/transform.ts
new file mode 100644
index 00000000..dcf30b92
--- /dev/null
+++ b/libraries/idb/src/relays/transform.ts
@@ -0,0 +1,194 @@
+import { merge as map } from 'object-mapper';
+import { NDKKind, NDKTag, NostrEvent, NDKRelayDiscovery, NDKRelayMeta } from '@nostr-dev-kit/ndk';
+import { geocodeTransform, hashString } from '../utils'
+import { ICheck, IRelay, IEvent, INip11, ISsl } from './tables';
+import { RelayDb } from './db';
+import { IGeoCode } from 'src/shared/tables';
+
+export type IdbReadyRelayData = {
+ check?: ICheck,
+ relay?: IRelay,
+ event?: IEvent,
+ nip11?: INip11,
+ geocodes?: IGeoCode[],
+ ssl?: ISsl
+}
+
+export const hashObject = async (obj: Record): Promise => {
+ const canonicalJson = JSON.stringify(obj, Object.keys(obj).sort());
+ const buffer = new TextEncoder().encode(canonicalJson);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
+ return hashHex;
+}
+
+export const relayMetaToICheck = ($event: NDKRelayMeta): ICheck => {
+ const { pubkey, created_at } = $event;
+ let parsedJson: Record = $event.all;
+ if(!parsedJson) parsedJson = {}
+
+ const hasGeo = parsedJson?.geo?.countryCode || parsedJson?.geo?.countryName
+
+ if(!$event) return {} as ICheck
+
+ parsedJson.id = $event.id as string
+ parsedJson.created_at = created_at as number;
+ parsedJson.pubkey = pubkey as string;
+ if($event && hasGeo)
+ parsedJson.geocode = $event.tags.filter(tag => tag[1] === 'regionCode' || tag[1] === 'countryCode').map(tag => tag[2])
+
+ const checkMapping = {
+ "id": "nid",
+ "pubkey": "monitorPubkey",
+ "nip11.pubkey": "operatorPubkey",
+ "other.network": "network",
+ "rtt.open": "open",
+ "rtt.read": "read",
+ "rtt.write": "write",
+ "geo.geohash": "geohash",
+ "geocode": "geocode",
+ "ssl.valid_to": "validTo",
+ "ssl.issuer": "issuer",
+ "dns.ipv4": "ipv4",
+ "dns.ipv6": "ipv6",
+ "dns.isp": "isp",
+ "nip11.supported_nips": "supportedNips",
+ "nip11.payment_required": "paymentRequired",
+ "nip11.auth_required": "authRequired",
+ "nip11.software": "software",
+ "nip11.version": "version",
+ "created_at": "createdAt",
+ }
+
+ const relayCheck: ICheck = map(parsedJson, checkMapping) as ICheck;
+
+ relayCheck.authRequired = relayCheck.authRequired? 1: 0
+ relayCheck.paymentRequired = relayCheck.paymentRequired? 1: 0
+ relayCheck.relay = $event.url as string;
+
+ return {...RelayDb.defaults(), ...relayCheck}
+}
+
+export const relayMetaToIRelay = ($event: NDKRelayMeta): IRelay => {
+ const { pubkey, created_at:lastSeen, url:relay } = $event;
+ const irelay: IRelay = {
+ relay: relay as string,
+ lastSeen: lastSeen as number,
+ network: $event.all?.other?.network as string
+ }
+ return {...RelayDb.defaults(), ...irelay}
+}
+
+export const relayMetaToIEvent = ($event: NDKRelayMeta): IEvent => {
+ const { pubkey:monitorPubkey, created_at, id:nid, url:relay, kind:maybeKind } = $event;
+ const createdAt = created_at as number;
+ const event: IEvent = {
+ nid,
+ kind: maybeKind as number,
+ monitorPubkey,
+ relay,
+ createdAt,
+ event: $event.rawEvent()
+ }
+ return {...RelayDb.defaults(), ...event}
+}
+
+const relayMetaToNip11 = async ($event: NDKRelayMeta): Promise => {
+ const { url, pubkey:monitorPubkey, id:nid, created_at:createdAt } = $event;
+ if(!createdAt) return
+ let json
+ try {
+ json = JSON.parse($event.content)
+ } catch(e){
+ return undefined
+ }
+
+ if(!json || !(Object.keys(json)?.length > 0)) return undefined
+
+ let hash = null;
+ try {
+ hash = await hashObject(json);
+ } catch (error) {
+ console.error('Error hashing object:', error);
+ }
+
+ const inip11 = {
+ relay: url as string,
+ nid,
+ monitorPubkey,
+ createdAt,
+ hash,
+ json,
+ }
+ return {...RelayDb.defaults(), ...inip11}
+}
+
+const relayMetaToSsl = async ($event: NDKRelayMeta): Promise => {
+ const { url:relay, pubkey:monitorPubkey, id:nid, created_at:createdAt } = $event;
+ const cert = $event.tags.find((tag: NDKTag) => tag[1] === 'pem_encoded')?.[2]
+
+ if(!cert || !relay) return undefined
+ const hash = await hashString(cert)
+
+ const ssl: ISsl = {
+ nid,
+ createdAt,
+ relay,
+ monitorPubkey,
+ hash,
+ cert,
+ }
+
+ return ssl
+}
+
+const nip66ToIGeoCodes = ($event: NDKRelayMeta | NDKRelayDiscovery): IGeoCode[] => {
+ const codes = geocodeTransform($event)
+ // if(codes.length) console.log(`codes:`, codes)
+ return codes
+}
+
+const relayMetaToIdb = async ($event: NDKRelayMeta): Promise => {
+ const res: IdbReadyRelayData = {}
+
+ const check = relayMetaToICheck($event)
+ const relay = relayMetaToIRelay($event)
+ const event = relayMetaToIEvent($event)
+ const nip11 = await relayMetaToNip11($event)
+ const geocodes = nip66ToIGeoCodes($event)
+ const ssl = await relayMetaToSsl($event)
+
+ if(check) res.check = check
+ if(relay) res.relay = relay
+ if(event) res.event = event
+ if(nip11) res.nip11 = nip11
+ if(geocodes) res.geocodes = geocodes
+ if(ssl) res.ssl = ssl
+
+ return res
+}
+
+const relayDiscoveryToIdb = async ($event: NDKRelayDiscovery): Promise => {
+ const res: IdbReadyRelayData = {}
+ const geocodes = nip66ToIGeoCodes($event)
+
+ if(geocodes) res.geocodes = geocodes
+
+ return res
+}
+
+export default async ( nostrEvent: NostrEvent ): Promise => {
+ const $event: NDKRelayMeta | NDKRelayDiscovery = nostrEvent.kind === NDKKind.RelayMeta? new NDKRelayMeta(undefined, nostrEvent): new NDKRelayDiscovery(undefined, nostrEvent)
+
+ let res: IdbReadyRelayData = {}
+
+ if($event.kind === NDKKind.RelayMeta) {
+ res = await relayMetaToIdb($event as NDKRelayMeta)
+ }
+ if($event.kind === NDKKind.RelayDiscovery) {
+ res = await relayDiscoveryToIdb($event as NDKRelayDiscovery)
+ }
+
+ return res
+}
\ No newline at end of file
diff --git a/libraries/idb/src/relays/worker.ts b/libraries/idb/src/relays/worker.ts
new file mode 100644
index 00000000..8ebaf4b9
--- /dev/null
+++ b/libraries/idb/src/relays/worker.ts
@@ -0,0 +1,18 @@
+import db from './db'
+
+const addToRelayDb = async (data: Record) => {
+ try {
+ await Promise.all([
+ db.relays.put(data?.relay || {}),
+ // db.relayChecks.put(data.relayChecks || {}),
+ // db.nip11s.put(data.nip11 || {}),
+ // db.ssls.put(data.ssl || {})
+ ]);
+ } catch (err) {
+ console.error('Failed to add mapped objects to DB:', err);
+ }
+}
+
+export default async ( jobData: Record ) => {
+ await addToRelayDb( jobData )
+}
\ No newline at end of file
diff --git a/libraries/idb/src/shared/tables.ts b/libraries/idb/src/shared/tables.ts
new file mode 100644
index 00000000..ff21e35a
--- /dev/null
+++ b/libraries/idb/src/shared/tables.ts
@@ -0,0 +1,36 @@
+
+
+import type { NostrEvent } from "@nostr-dev-kit/ndk";
+
+
+export interface IEvent {
+ nid: string;
+ monitorPubkey: string;
+ relay?: string | null;
+ event: NostrEvent;
+ createdAt: number | null;
+}
+
+export type TransformOptionsType = {
+ parsedJson: Record,
+ pubkey: string,
+ createdAt: number,
+ nostrEvent?: NostrEvent
+}
+
+export interface IGeoCode {
+ id?: number;
+ created_at: number;
+ code?: string | number;
+ type?: string;
+ format?: string;
+ length?: number;
+}
+
+// export interface IGeohash {
+// monitorPubkey: string;
+// relay?: string;
+// geohash?: string;
+// created_at?: number;
+// }
+
diff --git a/libraries/idb/src/test.ts b/libraries/idb/src/test.ts
new file mode 100644
index 00000000..ef3d129c
--- /dev/null
+++ b/libraries/idb/src/test.ts
@@ -0,0 +1,11 @@
+import type { IEvent } from './shared/tables';
+
+const testEvent: IEvent = {
+ nid: 'test',
+ monitorPubkey: 'pubkey',
+ relay: null,
+ event: {} as any,
+ createdAt: null,
+};
+
+console.log('Test import successful:', testEvent);
\ No newline at end of file
diff --git a/libraries/idb/src/types.ts b/libraries/idb/src/types.ts
new file mode 100644
index 00000000..6d45c5a6
--- /dev/null
+++ b/libraries/idb/src/types.ts
@@ -0,0 +1,14 @@
+export const enum ISO3166Type {
+ CountryCode = 'countryCode',
+ RegionCode = 'regionCode',
+}
+
+export const enum ISO3166Format {
+ Alpha = 'alpha',
+ Numeric = 'numeric',
+}
+
+export type GeoCodesRaw = {
+ countryCode?: string | string[]
+ regionCode?: string | string[];
+}
\ No newline at end of file
diff --git a/libraries/idb/src/types/@nostrwatch__kit.d.ts b/libraries/idb/src/types/@nostrwatch__kit.d.ts
new file mode 100644
index 00000000..caf2d8a1
--- /dev/null
+++ b/libraries/idb/src/types/@nostrwatch__kit.d.ts
@@ -0,0 +1,3 @@
+declare module '@nostrwatch/kit' {
+ export type MonitorRelayFetcher = MonitorRelayFetcher;
+}
\ No newline at end of file
diff --git a/libraries/idb/src/utils.ts b/libraries/idb/src/utils.ts
new file mode 100644
index 00000000..fe688b48
--- /dev/null
+++ b/libraries/idb/src/utils.ts
@@ -0,0 +1,101 @@
+import { NDKEventGeoCoded, NostrEvent, NDKEvent, NDKTag, NDKKind, NDKRelayMeta, NDKRelayDiscovery } from '@nostr-dev-kit/ndk';
+import { ISO3166Type, ISO3166Format, GeoCodesRaw } from './types';
+import { IGeoCode } from './shared/tables';
+import { Table } from 'dexie';
+
+export const nHoursAgo = (hrs: number): number => Math.floor((Date.now() - hrs * 60 * 60 * 1000) / 1000);
+
+export const normalizeUrl = (url: string) => {
+ const parsedUrl = new URL(url);
+ parsedUrl.search = '';
+ parsedUrl.hash = '';
+ return parsedUrl.toString();
+};
+
+export const extractGeoCodesFromRelayMeta = ( event: NDKRelayMeta, type: ISO3166Type): (string | number| undefined)[] => {
+ return event.tags
+ .filter((tag: NDKTag) => tag[0] === 'geo' && tag[1] === type)
+ .map((tag: NDKTag) => tag[2])
+}
+
+export const extractGeoCodesFromRelayDiscovery = ( event: NDKRelayDiscovery, type: ISO3166Type ): (string | number | undefined)[] => {
+ return event.tags
+ .filter((tag: NDKTag) => tag[0] === 'G' && tag[2] === type)
+ .map((tag: NDKTag) => tag[1])
+}
+
+export const extractGeoCodes = ( event: NDKRelayMeta | NDKRelayDiscovery ): GeoCodesRaw => {
+ let countryCode: (string | number | undefined)[] = [];
+ let regionCode: (string | number | undefined)[] = [];
+ if(event.kind === NDKKind.RelayMeta) {
+ countryCode = extractGeoCodesFromRelayMeta(event as NDKRelayMeta, ISO3166Type.CountryCode)
+ regionCode = extractGeoCodesFromRelayMeta(event as NDKRelayMeta, ISO3166Type.RegionCode)
+ }
+ if(event.kind === NDKKind.RelayDiscovery) {
+ countryCode = extractGeoCodesFromRelayDiscovery(event as NDKRelayDiscovery, ISO3166Type.CountryCode)
+ regionCode = extractGeoCodesFromRelayDiscovery(event as NDKRelayDiscovery, ISO3166Type.RegionCode)
+ }
+
+ return { countryCode, regionCode } as GeoCodesRaw
+}
+
+export const parseGeocode = (type: ISO3166Type, code: string): IGeoCode => {
+ const isNumeric = !isNaN(Number(code))
+ const ignoreLength = type === ISO3166Type.RegionCode? true: false || isNumeric
+ return {
+ code,
+ type: type,
+ format: isNumeric? ISO3166Format.Numeric: ISO3166Format.Alpha,
+ length: ignoreLength? -1: code.length
+ }
+}
+
+export const parseGeocodes = (codes: string | string[] | undefined, type: ISO3166Type): IGeoCode[] | undefined => {
+ const geocodeEntries: IGeoCode[] = [];
+ if(codes === 'string') codes = [codes];
+ if(!codes) return
+ (codes as string[]).forEach( (code: string) => {
+ geocodeEntries.push( parseGeocode(type, code) )
+ })
+ return geocodeEntries
+}
+
+export const geocodeTransform = ( event: NDKRelayMeta | NDKRelayDiscovery ): IGeoCode[] => {
+ const codes: GeoCodesRaw = extractGeoCodes(event)
+ const cc = parseGeocodes(codes.countryCode, ISO3166Type.CountryCode)
+ const rc = parseGeocodes(codes.regionCode, ISO3166Type.RegionCode)
+ const res: IGeoCode[] = []
+ if(cc) res.push(...cc)
+ if(rc) res.push(...rc)
+ return res;
+}
+
+export const allOf = (table: Table, multiValueProp: keyof T & string, keys: (T[keyof T & string])[]): Promise => {
+ if (keys.length === 0) return Promise.resolve([]);
+
+ const [dbKey, ...filteredKeys] = keys;
+ return table.where(multiValueProp).equals(dbKey as any).toArray().then((dbResult: T[]) =>
+ filteredKeys.reduce((result: T[], key: T[keyof T & string]) =>
+ result.filter((doc: T) => Array.isArray(doc[multiValueProp]) && (doc[multiValueProp] as any[]).includes(key)),
+ dbResult
+ )
+ );
+};
+
+export const hashString = async (input: string) => {
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) {
+ // Browser environment
+ const encoder = new TextEncoder();
+ const data = encoder.encode(input);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
+ return hashHex;
+ } else if (typeof require === 'function') {
+ // Node.js environment
+ const crypto = require('crypto');
+ return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
+ } else {
+ throw new Error('Environment not supported');
+ }
+}
\ No newline at end of file
diff --git a/libraries/idb/tsconfig.json b/libraries/idb/tsconfig.json
new file mode 100644
index 00000000..cf2e518f
--- /dev/null
+++ b/libraries/idb/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "lib": ["es2019", "dom"],
+ "outDir": "./dist",
+ "module": "commonjs",
+ "target": "es6",
+ "declaration": true,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["node"],
+ "baseUrl": ".",
+ "typeRoots": ["./node_modules/@types", "./src/types"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["./dist", "**/*.test.ts", "./node_modules", "docs"]
+}
diff --git a/libraries/idb/vite.config.js b/libraries/idb/vite.config.js
new file mode 100644
index 00000000..bf92e6f5
--- /dev/null
+++ b/libraries/idb/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import { nodePolyfills } from 'vite-plugin-node-polyfills'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ nodePolyfills({
+ include: ['crypto']
+ })
+ ],
+})
\ No newline at end of file
diff --git a/libraries/kit/.gitignore b/libraries/kit/.gitignore
new file mode 100644
index 00000000..6aac1d13
--- /dev/null
+++ b/libraries/kit/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+yalc.lock
+.yalc
+docs
+dist
+.secrets
\ No newline at end of file
diff --git a/libraries/kit/README.md b/libraries/kit/README.md
new file mode 100644
index 00000000..fdbfc799
--- /dev/null
+++ b/libraries/kit/README.md
@@ -0,0 +1,57 @@
+> ATTENTION! This repo is under heavy development and does not work yet. There are a number of upstream dependencies that are not yet published.
+
+# @nostrwatch/ndk
+
+An NDK extension that adds advanced support to fetch complete [NIP-66]() datasets
+
+[![semver](https://img.shields.io/npm/v/nostrwatch-ndk)](https://github.com/sandwichfarm/nostrwatch-ndk/releases/latest)
+[![cov](https://sandwichfarm.github.io/nostrwatch-ndk/badges/coverage.svg)](https://github.com/sandwichfarm/nostrwatch-ndk/actions)
+[![test](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/buildtest.yaml/badge.svg)](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/buildtest.yaml)
+[![publish](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/publish.yaml/badge.svg)](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/publish.yaml)
+[![docs](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/docs.yaml/badge.svg)](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/docs.yaml)
+[![covgen](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/coverage.yaml/badge.svg)](https://github.com/sandwichfarm/nostrwatch-ndk/actions/workflows/coverage.yaml)
+![npm bundle size](https://img.shields.io/bundlephobia/minzip/nostrwatch-ndk)
+![npm bundle size](https://img.shields.io/bundlephobia/min/nostrwatch-ndk)
+
+## TODO
+- [ ] NIP-66 NDK PR
+- [x] Extend upstream NDKEvents
+- [x] Implement Fetchers
+- [x] Implement Helpers
+
+## Install
+
+```
+npm install @nostrwatch/ndk
+pnpm install @nostrwatch/ndk
+yarn add @nostrwatch/ndk
+```
+
+## Usage
+```typescript
+import { FetchRelayMonitors, FetchRelays } from "@nostrwatch/ndk";
+..
+```
+
+## Testing
+```
+npm run test
+pnpm run test
+yarn test
+```
+
+## Build Docs
+```
+npx typedoc src/index.ts --out docs --ignoreCompilerErrors
+```
+
+Serve them with
+```
+npm run serve-docs
+pnpm run serve-docs
+yarn serve-docs
+```
+
+## Links
+- [Reference Docs](https://nostrwatch-ndk.github.io)
+- Guides [WIP]
\ No newline at end of file
diff --git a/libraries/kit/adapters/kit-adapter-idb/package.json b/libraries/kit/adapters/kit-adapter-idb/package.json
new file mode 100644
index 00000000..26df9f7d
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@nostrwatch/kit-adapter-idb",
+ "version": "0.0.1",
+ "main": "src/index.js",
+ "license": "MIT",
+ "type": "module",
+ "dependencies": {
+ "@nostrwatch/idb": "0.0.1",
+ "@nostrwatch/kit": "0.0.1",
+ "murmurhash": "2.0.1",
+ "promise-deferred": "2.0.4",
+ "web-worker": "1.3.0"
+ },
+ "devDependencies": {
+ "@types/node": "20.14.8",
+ "vite-plugin-worker": "1.0.5"
+ },
+ "scripts": {
+ "build": "tsc",
+ "test": "vitest",
+ "test-ui": "vitest --ui --coverage.enabled=true",
+ "start": "node dist/app.js",
+ "dev": "tsc-watch --onSuccess 'node dist/app.js'",
+ "lint": "eslint . --ext .ts",
+ "coverage": "vitest run --reporter=junit --coverage.reporter=json-summary --coverage.enabled=true",
+ "docs": "npx typedoc src/index.ts --out ./docs --ignoreCompilerErrors --plugin typedoc-plugin-ensure-internal",
+ "serve-docs": "http-server ./docs"
+ },
+ "files": [
+ "README.md",
+ "dist",
+ "src",
+ "tsconfig.json",
+ ".yalc"
+ ]
+}
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/index.ts b/libraries/kit/adapters/kit-adapter-idb/src/index.ts
new file mode 100644
index 00000000..1daff4dd
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/src/index.ts
@@ -0,0 +1,2 @@
+export * from './monitors'
+export * from './relays'
\ No newline at end of file
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/monitors/index-w-worker.ts b/libraries/kit/adapters/kit-adapter-idb/src/monitors/index-w-worker.ts
new file mode 100644
index 00000000..85788d8b
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/src/monitors/index-w-worker.ts
@@ -0,0 +1,108 @@
+import NDK from "@nostr-dev-kit/ndk";
+import Deferred from "promise-deferred";
+
+import { MonitorRelayFetcher, KitCacheRelayMonitorInterface, RelayMonitorSetExt } from "@nostrwatch/kit";
+import { IMonitor, type MonitorDb, monitorDb, MonitorTransform } from "@nostrwatch/idb";
+
+export class MonitorCacheIdbAdapter implements KitCacheRelayMonitorInterface {
+ db: MonitorDb;
+ ndk: NDK | undefined;
+ worker: Worker | undefined;
+ workPromises: Map = new Map();
+
+ constructor(ndk?: NDK, worker?: Worker) {
+ this.db = monitorDb;
+ this.ndk = ndk;
+ this.worker = worker;
+ this.setupWorkerListeners();
+ }
+
+ setupWorkerListeners() {
+ if (this.worker) {
+ this.worker.onmessage = (event) => {
+ const { id, success, error } = event.data;
+ const workPromise = this.workPromises.get(id);
+ if (workPromise) {
+ if (success) {
+ workPromise.resolve(true);
+ } else {
+ workPromise.reject(error);
+ }
+ this.workPromises.delete(id);
+ }
+ };
+ }
+ }
+
+ async set(monitorEvent: MonitorRelayFetcher): Promise {
+ const method = 'set';
+ const created_at = monitorEvent.created_at as number;
+ const id = monitorEvent.id;
+ const existing = this.db.monitors
+ .where('monitorPubkey').equals(monitorEvent.pubkey)
+ .and(m => m.created_at >= created_at);
+
+ const request = MonitorTransform(monitorEvent);
+
+ if (!this.worker) {
+ this.worker = await this.workerPromise;
+ }
+
+ const message = { id, method, request };
+
+ console.log('to worker: setting monitor', message);
+
+ this.worker?.postMessage(message);
+ this.workPromises.set(id, new Deferred());
+
+ const response = await this.workPromises.get(id).promise;
+
+ console.log('from worker: setting monitor', response);
+
+ return response.success as boolean;
+ }
+
+ async get(monitorPubkey: string): Promise {
+ const monitor: IMonitor | undefined = await this.db.monitors.where({ monitorPubkey }).first();
+ if (!monitor) return undefined;
+ return new MonitorRelayFetcher(this.ndk, monitor.event);
+ }
+
+ async remove(monitorPubkey: string): Promise {
+ const method = 'remove';
+ const request = monitorPubkey;
+ const id = monitorPubkey;
+
+ if (!this.worker) {
+ this.worker = await this.workerPromise;
+ }
+
+ this.worker?.postMessage({ id, method, request });
+ this.workPromises.set(id, new Deferred());
+
+ const response = await this.workPromises.get(id).promise;
+
+ return response.success as boolean;
+ }
+
+ async keys(): Promise> {
+ return new Set(await this.db.monitors.toCollection().primaryKeys());
+ }
+
+ async load(monitorEvents: RelayMonitorSetExt): Promise {
+ if (!monitorEvents) return;
+ const arr = Array.from(monitorEvents);
+ arr.forEach(monitor => this.set(monitor));
+ }
+
+ async dump(): Promise {
+ const monitors: MonitorRelayFetcher[] = (await this.db.monitors.toArray()).map((monitor) => new MonitorRelayFetcher(this.ndk, monitor.event));
+ console.log('dump wtf', await this.db.monitors.toArray());
+ return new Set(monitors) as RelayMonitorSetExt;
+ }
+
+ async reset(): Promise {
+ this.db.monitors.clear();
+ }
+}
+
\ No newline at end of file
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/monitors/index.ts b/libraries/kit/adapters/kit-adapter-idb/src/monitors/index.ts
new file mode 100644
index 00000000..e9bb7bca
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/src/monitors/index.ts
@@ -0,0 +1,96 @@
+import NDK from "@nostr-dev-kit/ndk";
+import { MonitorRelayFetcher, KitCacheRelayMonitorInterface, RelayMonitorSetExt } from "@nostrwatch/kit";
+import { IMonitor, type MonitorDb, monitorDb, MonitorTransform } from "@nostrwatch/idb";
+
+import * as fastq from "fastq";
+import type { queueAsPromised } from "fastq";
+
+const setQueue: queueAsPromised = fastq.promise(setWorker, 1)
+
+async function setWorker (monitor: IMonitor): Promise {
+ let err = false
+
+ const onErr = () => {
+ err = true
+ console.error('Error setting monitor data', err)
+ }
+ await monitorDb.monitors.put(monitor).catch(onErr)
+}
+
+export class MonitorCacheIdbAdapter implements KitCacheRelayMonitorInterface {
+
+ db: MonitorDb;
+ ndk: NDK | undefined;
+
+ constructor(ndk?: NDK) {
+ this.db = monitorDb;
+ this.ndk = ndk
+ }
+
+ async set( monitorEvent: MonitorRelayFetcher ): Promise {
+ const data = MonitorTransform(monitorEvent);
+ setQueue.push(data.monitor).catch((err) => console.error(err))
+ // const created_at = monitorEvent.created_at as number
+
+ // const data = MonitorTransform(monitorEvent);
+ let err = false
+
+ // const onErr = () => {
+ // err = true
+ // console.error('Error setting monitor data', err)
+ // }
+ // const response = await this.db.monitors.put(data.monitor).catch(onErr)
+
+ // if(existing) {
+ // console.log('!!! monitor exists', existing)
+ // await existing
+ // .modify( function(monitor){
+ // monitor.isActive = data.monitor.isActive
+ // })
+ // .catch(onErr)
+ // }
+ // else {
+ // console.log('!!! monitor does not exist')
+ // await this.db.monitors.put(data.monitor).catch(onErr)
+ // }
+
+ return !err
+ }
+
+ async get( monitorPubkey: string ): Promise {
+ const monitor: IMonitor | undefined = await this.db.monitors.where({ monitorPubkey }).first()
+ if(!monitor?.event) return undefined
+ return new MonitorRelayFetcher(this.ndk, monitor.event)
+ }
+
+ async remove( monitorPubkey: string ): Promise {
+ const promises: Promise[] = []
+ let err = false
+ const onErr = () => {
+ err = true
+ console.error('Error deleting monitor data', err)
+ }
+ await this.db.monitors.delete(monitorPubkey).catch(onErr)
+ return !err
+ }
+
+ async keys(): Promise> {
+ return new Set(await this.db.monitors.toCollection().primaryKeys())
+ }
+
+ async load( monitorEvents: RelayMonitorSetExt ): Promise {
+ if(!monitorEvents) return;
+ const arr = Array.from(monitorEvents)
+ arr.forEach( monitor => this.set(monitor))
+ }
+
+ async dump(): Promise {
+ const monitors: MonitorRelayFetcher[] = (await this.db.monitors.toArray()).map( ( monitor ) => new MonitorRelayFetcher(this.ndk, monitor.event))
+ console.log('dump wtf', await this.db.monitors.toArray())
+ return new Set(monitors) as RelayMonitorSetExt
+ }
+
+ async reset(): Promise {
+ this.db.monitors.clear();
+ }
+}
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/monitors/worker.ts b/libraries/kit/adapters/kit-adapter-idb/src/monitors/worker.ts
new file mode 100644
index 00000000..acf0a779
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/src/monitors/worker.ts
@@ -0,0 +1,43 @@
+import { monitorDb } from "@nostrwatch/idb";
+
+let failure = false
+let error: Error | undefined;
+
+const onErr = (_error: Error) => {
+ failure = true
+ error = _error
+ console.error('Worker: Error setting monitor data', error)
+}
+
+self.addEventListener('message', (e) => {
+ console.log('Worker received message:', e.data);
+ const { id, method, request } = e.data;
+ if (method === 'set') {
+ set(id, request);
+ }
+ if (method == 'remove') {
+ remove(id, request);
+ }
+});
+
+const set = async (id: string, request: any) => {
+ console.log('Worker: set', id);
+ try {
+ await monitorDb.monitors.put(request.monitor).catch(onErr);
+ } catch (error) {
+ console.error('Worker: Error setting monitor data', error);
+ failure = true;
+ }
+ postMessage({ id, method: 'set', subject: request.monitorPubkey, success: !failure, error });
+};
+
+const remove = async (id: string, request: string) => {
+ console.log('Worker: remove', id);
+ try {
+ await monitorDb.monitors.delete(request).catch(onErr);
+ } catch (error) {
+ console.error('Worker: Error removing monitor data', error);
+ failure = true
+ }
+ postMessage({ id, method: 'remove', subject: request, success: !failure, error });
+};
\ No newline at end of file
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/relays/index.ts b/libraries/kit/adapters/kit-adapter-idb/src/relays/index.ts
new file mode 100644
index 00000000..8798cb67
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/src/relays/index.ts
@@ -0,0 +1,211 @@
+import NDK, { NDKEvent, NDKRelayMeta, RelayMetaSet } from "@nostr-dev-kit/ndk";
+import { KitCacheRelayMetaInterface } from "@nostrwatch/kit";
+import { ICheck, IEvent, INip11, IRelay, ISsl, relayDb, RelayDb, relayMetaToIEvent, RelayTransform } from "@nostrwatch/idb";
+import murmurhash from "murmurhash";
+
+import * as fastq from "fastq";
+import type { queueAsPromised } from "fastq";
+import { IGeoCode } from "@nostrwatch/idb/src/shared/tables";
+
+const setQueue: queueAsPromised = fastq.promise(setWorker, 10)
+
+async function setWorker (event: IEvent): Promise {
+ let err = false
+
+ const onErr = () => {
+ err = true
+ console.error('Error setting monitor data', err)
+ }
+ await relayDb.events.put(event).catch(onErr)
+}
+
+export class RelayCacheIdbAdapter implements KitCacheRelayMetaInterface {
+
+ db: RelayDb;
+ ndk: NDK | undefined;
+
+ constructor(ndk?: NDK) {
+ //console.log('RelayCacheIdbAdapter constructor')
+ this.db = relayDb;
+ this.ndk = ndk
+ }
+
+ async set( relayMetaEvent: NDKRelayMeta ): Promise {
+ let err = false
+
+ const onErr = () => {
+ err = true
+ console.error('Error setting monitor data', err)
+ }
+ // console.log('adding from worker')
+ await relayDb.events.put(relayMetaToIEvent( relayMetaEvent )).catch(onErr)
+
+ return !err
+ // const record = relayMetaToIEvent( relayMetaEvent )
+ // await setQueue.push(record).catch((err) => console.error(err))
+ // return true
+ }
+
+ async get( relay: string, monitorPubkey: string ): Promise {
+ const rid = murmurhash.v3( relay )
+ const event = await this.db.events.get({rid, monitorPubkey})
+ return new NDKRelayMeta(this.ndk, event?.event)
+ }
+
+ async list(relay: string): Promise {
+ const rid = murmurhash.v3( relay )
+ const events = await this.db.events.where({rid}).toArray()
+ return events.map( ( event ) => new NDKRelayMeta(this.ndk, event.event))
+ }
+
+ async remove( relay: string ): Promise {
+ const rid = murmurhash.v3( relay )
+ return (await this.db.events.where({rid}).delete())? true : false
+ }
+
+ async keys(): Promise> {
+ const keys = (await this.db.relays.toArray())?.map(relay => relay.relay)
+ return new Set(keys)
+ }
+
+ async load( relayMetaEvents: RelayMetaSet ): Promise> {
+ let monitorPubkey: string | undefined;
+ const begin = Date.now()
+ if(!relayMetaEvents) return;
+ //console.log(`load(): loading ${relayMetaEvents.size} relays`)
+ const promises: Promise[] = []
+ const iEvents: IEvent[] = []
+ Array.from(relayMetaEvents).forEach((meta: NDKRelayMeta) => {
+ promises.push(this.set(meta))
+ iEvents.push( relayMetaToIEvent( meta ) )
+ if(!monitorPubkey) monitorPubkey = meta.pubkey
+ })
+ await Promise.allSettled(promises)
+
+ await new Promise( resolve => setTimeout(resolve, 1000) )
+
+ console.log(`processing ${iEvents.length} events`)
+
+ return await updateRelayData(iEvents)
+
+ console.log(`${relayMetaEvents?.size} loaded. Took ${Math.round((Date.now() - begin)/1000)} seconds.`)
+ }
+
+ async dump(monitorPubkey: string): Promise {
+ const ievents: IEvent[] = (await this.db.events.toArray())
+ const metas: NDKRelayMeta[] = ievents.map( ( meta ) => new NDKRelayMeta(this.ndk, meta.event))
+ return new Set(metas) as RelayMetaSet
+ }
+
+ async reset(): Promise {
+ await Promise.allSettled([
+ this.db.relays.clear(),
+ this.db.checks.clear(),
+ this.db.pastChecks.clear(),
+ this.db.events.clear()
+ ])
+ }
+}
+
+export const updateRelayData = async (events: IEvent[]) => {
+ const stats: Record = {}
+ const t = tables(events)
+ for(let i = 0; i < t.length; i++) {
+ const table = t[i]
+ const {tableName, transformProperty, events} = table
+ stats[tableName] = await populateData(tableName as keyof RelayDb, transformProperty as keyof IdbReadyRelayData, events)
+ }
+ return stats
+}
+
+const tables = (events: IEvent[]) => {
+ return [
+ {
+ tableName: 'relays',
+ transformProperty: 'relay',
+ events
+ },
+ {
+ tableName: 'checks',
+ transformProperty: 'check',
+ events
+ },
+ {
+ tableName: 'nip11s',
+ transformProperty: 'nip11',
+ events
+ },
+ {
+ tableName: 'ssls',
+ transformProperty: 'ssl',
+ events
+ }
+ ]
+}
+
+
+interface IdbReadyRelayData {
+ event?: IEvent;
+ relay?: IRelay;
+ check?: ICheck;
+ ssl?: ISsl;
+ geocodes?: IGeoCode;
+ nip11?: INip11;
+}
+
+
+// interface RelayDb {
+// [tableName: string]: {
+// put: (item: any) => Promise;
+// };
+// }
+
+interface TableOperations {
+ put: (item: T) => Promise;
+ bulkPut: (items: T[]) => Promise;
+}
+
+// interface RelayDb {
+// ssls: TableOperations;
+// geocodes: TableOperations;
+// relays: TableOperations;
+// checks: TableOperations;
+// events: TableOperations & {
+// where: (criteria: object) => { toArray: () => Promise };
+// put: (item: IEvent) => Promise;
+// };
+// }
+
+// Generic function to populate a database table from event data
+export const populateData = async (
+ tableName: T,
+ transformProperty: keyof IdbReadyRelayData, // Adjust if the property name needs to be dynamic
+ events: IEvent[]
+): Promise => {
+ let mostRecent = 0;
+
+ events.forEach( async (event) => {
+ const timestamp = await processEvent(event, tableName, transformProperty)
+ if (timestamp && timestamp > mostRecent) {
+ mostRecent = timestamp;
+ }
+ })
+ return mostRecent;
+};
+
+export const processEvent = async (event: IEvent, tableName: T, transformProperty: keyof IdbReadyRelayData): Promise => {
+ const data = await RelayTransform(event.event);
+ const item = data[transformProperty] as any; // Cast to any if specific typing is not feasible
+ if (item) {
+ console.log(`adding ${String(transformProperty)} ${item.nid}`);
+ if(item instanceof Array) {
+ relayDb[tableName].bulkPut(item);
+ // console.log('finished bulk put', tableName, transformProperty)
+ }
+ else {
+ relayDb[tableName].put(item);
+ // console.log('finished put', tableName, transformProperty)
+ }
+ return item.createdAt;
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/adapters/kit-adapter-idb/src/relays/worker.ts b/libraries/kit/adapters/kit-adapter-idb/src/relays/worker.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/libraries/kit/adapters/kit-adapter-idb/tsconfig.json b/libraries/kit/adapters/kit-adapter-idb/tsconfig.json
new file mode 100644
index 00000000..f2ea7ffe
--- /dev/null
+++ b/libraries/kit/adapters/kit-adapter-idb/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "lib": ["es2022", "dom"],
+ "outDir": "./dist",
+ "module": "esnext",
+ "target": "es2022",
+ "declaration": true,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["node"],
+ "baseUrl": "."
+ },
+ "include": ["src/**/*", ".yalc"],
+ "exclude": ["./dist", "**/*.test.ts", "./node_modules", "docs"]
+}
\ No newline at end of file
diff --git a/libraries/kit/package.json b/libraries/kit/package.json
new file mode 100644
index 00000000..a0553f9c
--- /dev/null
+++ b/libraries/kit/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@nostrwatch/kit",
+ "version": "0.0.1",
+ "description": "NIP-66 relay discoverability via NDK",
+ "main": "src/index.ts",
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "test": "vitest",
+ "test-ui": "vitest --ui --coverage.enabled=true",
+ "start": "node dist/app.js",
+ "dev": "tsc-watch --onSuccess 'node dist/app.js'",
+ "lint": "eslint . --ext .ts",
+ "coverage": "vitest run --reporter=junit --coverage.reporter=json-summary --coverage.enabled=true",
+ "docs": "npx typedoc src/index.ts --out ./docs --ignoreCompilerErrors --plugin typedoc-plugin-ensure-internal",
+ "serve-docs": "http-server ./docs"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@nostr-dev-kit/ndk": "2.8.2 ",
+ "@nostr-fetch/adapter-ndk": "0.15.1",
+ "import2": "1.0.3",
+ "nostr-fetch": "0.15.1"
+ },
+ "devDependencies": {
+ "@types/node": "20.14.8",
+ "@vitest/coverage-v8": "1.6.0",
+ "@vitest/ui": "1.6.0",
+ "http-server": "14.1.1",
+ "tsdoc": "0.0.4",
+ "typedoc": "0.26.2",
+ "typedoc-plugin-ensure-internal": "https://github.com/deap82/typedoc-plugin-ensure-internal",
+ "typescript": "5.5.2",
+ "vite": "5.3.1",
+ "vite-tsconfig-paths": "4.3.2",
+ "vitest": "1.6.0"
+ },
+ "files": [
+ "README.md",
+ "dist",
+ "src",
+ "tsconfig.json",
+ ".yalc"
+ ]
+}
diff --git a/libraries/kit/src/cache/default-monitor-cache.ts b/libraries/kit/src/cache/default-monitor-cache.ts
new file mode 100644
index 00000000..17d44e48
--- /dev/null
+++ b/libraries/kit/src/cache/default-monitor-cache.ts
@@ -0,0 +1,55 @@
+import { MonitorRelayFetcher, RelayMonitorSetExt } from "..";
+
+type RelayMonitorMap = Map;
+
+export interface KitCacheRelayMonitorInterface {
+ set: (monitorEvent: MonitorRelayFetcher) => Promise;
+ get: (monitorPubkey: string) => Promise;
+ remove: (monitorPubkey: string) => Promise;
+ keys: () => Promise>;
+ load: (monitorEvents: RelayMonitorSetExt) => Promise;
+ dump: () => Promise;
+ reset: () => Promise;
+ [key: string]: any;
+}
+
+export class KitCacheRelayMonitorDefault implements KitCacheRelayMonitorInterface {
+ monitors: RelayMonitorMap;
+
+ constructor() {
+ this.monitors = new Map();
+ }
+
+ async set( monitorEvent: MonitorRelayFetcher ): Promise {
+ this.monitors.set( monitorEvent.pubkey, monitorEvent as MonitorRelayFetcher );
+ return true;
+ }
+
+ async get( monitorPubkey: string ): Promise {
+ return this.monitors.get(monitorPubkey)
+ }
+
+ async remove( monitorPubkey: string ): Promise {
+ return this.monitors.delete(monitorPubkey);
+ }
+
+ async keys(): Promise> {
+ const arr = Array.from(this.monitors.values());
+ const urls = arr.map(event => event.tags.find(tag => tag[0] === 'd')?.[1])
+ return new Set(urls.filter((tag): tag is string => tag !== undefined));
+ }
+
+ async load( monitorEvents: RelayMonitorSetExt ): Promise {
+ monitorEvents?.forEach( event => {
+ this.monitors.set( event.pubkey, event as MonitorRelayFetcher );
+ })
+ }
+
+ async dump(): Promise {
+ return new Set(this.monitors.values())
+ }
+
+ async reset(): Promise {
+ this.monitors.clear();
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/src/cache/default-relay-cache.ts b/libraries/kit/src/cache/default-relay-cache.ts
new file mode 100644
index 00000000..a2b24f60
--- /dev/null
+++ b/libraries/kit/src/cache/default-relay-cache.ts
@@ -0,0 +1,103 @@
+import { NDKRelayMeta, RelayMetaSet } from "@nostr-dev-kit/ndk";
+
+// Define the KitCacheRelayMetaInterface interface
+export interface KitCacheRelayMetaInterface {
+ set: (relayMetaEvent: NDKRelayMeta) => Promise;
+ get: (relayUrl: string, monitorPubkey: string) => Promise;
+ remove: (relayUrl: string) => Promise;
+ keys: () => Promise>;
+ load: (relayUrl: RelayMetaSet) => Promise>;
+ dump: (monitorPubkey: string) => Promise;
+ reset: () => Promise;
+ [key: string]: any;
+ }
+
+type MonitorRelayMap = Map
+type RelayMap = Map
+
+export class KitCacheRelayMetaDefault implements KitCacheRelayMetaInterface {
+ private _events: RelayMap
+
+ constructor() {
+ this._events = new Map()
+ }
+
+ get relays(): RelayMap {
+ return this._events
+ }
+
+ maybeInitRelay(relayUrl: string) {
+ const relayMap = this.relays.get(relayUrl)
+ if (!relayMap) {
+ const map: MonitorRelayMap = new Map()
+ this.relays.set(relayUrl, map)
+ }
+ }
+
+ async set( relayMetaEvent: NDKRelayMeta ): Promise {
+ const {url} = relayMetaEvent
+ if(!url) return false
+ this.maybeInitRelay(url)
+ this.relays.get(url)?.set(relayMetaEvent.pubkey, relayMetaEvent)
+ return true
+ }
+
+ async get( relayUrl: string, monitorPubkey: string ): Promise {
+ return this.relays.get(relayUrl)?.get(monitorPubkey)
+ }
+
+ async list(relayUrl: string): Promise {
+ const values = this.relays.get(relayUrl)?.values()
+ if(!values) return
+ return Array.from(values)
+ }
+
+ async keys(): Promise> {
+ return new Set(Array.from(this.relays.keys()));
+ }
+
+ async load( relayEvents: RelayMetaSet ): Promise> {
+ console.log('default cache')
+ let mostRecent = 0
+ relayEvents?.forEach( relayMetaEvent => {
+ const {url, created_at} = relayMetaEvent
+ if(!url) return false
+ this.relays.get( url )?.set( relayMetaEvent.pubkey, relayMetaEvent as NDKRelayMeta );
+ if(created_at && created_at > mostRecent) {
+ mostRecent = created_at
+ }
+ });
+ return { events: mostRecent }
+ }
+
+ async dump( monitorPubkey: string ): Promise {
+ const relayEvents: RelayMetaSet = new Set()
+ this.relays.forEach( (relayMap, relayUrl) => {
+ relayMap.forEach( (relayMeta, pubkey) => {
+ if( pubkey === monitorPubkey ) {
+ relayEvents.add(relayMeta)
+ }
+ })
+ })
+ return relayEvents
+ }
+
+ async dumpAll(): Promise {
+ const relayEvents: RelayMetaSet = new Set()
+ this.relays.forEach( (relayMap, relayUrl) => {
+ relayMap.forEach( (relayMeta) => {
+ relayEvents.add(relayMeta)
+ })
+ })
+ return relayEvents
+ }
+
+ async remove(relayUrl: string): Promise {
+ this.relays.delete(relayUrl);
+ return true
+ }
+
+ async reset(): Promise {
+ this.relays.clear();
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/src/cache/index.ts b/libraries/kit/src/cache/index.ts
new file mode 100644
index 00000000..7b19f780
--- /dev/null
+++ b/libraries/kit/src/cache/index.ts
@@ -0,0 +1,2 @@
+export * from './default-monitor-cache';
+export * from './default-relay-cache';
\ No newline at end of file
diff --git a/libraries/kit/src/fetchers/monitor-fetcher.ts b/libraries/kit/src/fetchers/monitor-fetcher.ts
new file mode 100644
index 00000000..95ef894a
--- /dev/null
+++ b/libraries/kit/src/fetchers/monitor-fetcher.ts
@@ -0,0 +1,181 @@
+import type NDK from '@nostr-dev-kit/ndk';
+import { NDKFilter, NDKEventGeoCoded as EventGeoCoded, NDKKind } from '@nostr-dev-kit/ndk';
+import type { MonitorRelayFetcher, RelayMonitorSetExt, RelayMonitorDiscoveryFilters, RelayMonitorCriterias } from './monitor-relay-fetcher';
+import { castSet, castSetRelayMonitorFetchers } from '../utils';
+
+export const MonitorFetcherOptionsDefaults: MonitorFetcherOptions = {
+ activeOnly: true
+}
+
+export type MonitorFetcherOptions = {
+ customFilter?: NDKFilter;
+ builtinFilter?: RelayMonitorDiscoveryFilters;
+ criterias?: RelayMonitorCriterias;
+ nearby?: EventGeoCodedGeospatialOptions;
+ activeOnly?: boolean;
+}
+
+export type EventGeoCodedGeospatialOptions = {
+ geohash: string;
+ maxPrecision?: number;
+ minPrecision?: number;
+ minResults?: number;
+ recurse?: boolean;
+}
+
+export class MonitorFetcher {
+ private _ndk: NDK;
+ private _options: MonitorFetcherOptions;
+
+ constructor( ndk: NDK, options: MonitorFetcherOptions = MonitorFetcherOptionsDefaults ){
+ this._options = options || {} as MonitorFetcherOptions;
+ this._ndk = ndk;
+ }
+
+ get ndk(): NDK {
+ return this._ndk;
+ }
+
+ set ndk(ndk: NDK) {
+ this._ndk = ndk;
+ }
+
+ get options(): MonitorFetcherOptions {
+ return this._options;
+ }
+
+ set options( options: MonitorFetcherOptions ) {
+ this._options = options;
+ }
+
+ /**
+ * @description Fetches monitors with optional filter and activity discriminator.
+ *
+ * @param {NDKFilter} filter The NDK instance to use for fetching events.
+ * @param {boolean} activeOnly Return only active monitors.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ public async monitors( filter?: NDKFilter, activeOnly: boolean = true ): Promise {
+ if(!this.ndk){
+ return undefined;
+ }
+ if(activeOnly){
+ return this.activeOnly(filter)
+ }
+ return this.fetchMonitors(filter);
+ }
+
+ /**
+ * @description Fetches monitors and sorts by distance with a given geohash
+ *
+ * @param {NDK} ndk The NDK instance to use for fetching events.
+ * @param {NDKFilter} filter An optional, additional filter to ammend to the default filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ public async activeOnly( filter?: NDKFilter ): Promise {
+ if(!this.ndk){
+ return undefined;
+ }
+ const events: RelayMonitorSetExt = await this.fetchMonitors(filter);
+ if(!events?.size) return undefined;
+ let active = await MonitorFetcher.filterActiveMonitors( events );
+ return active?.size? active: new Set();
+ }
+
+ /**
+ * @description Fetches monitors by a MonitorTag
+ *
+ * @param {RelayMonitorSetExt} monitors A set of `MonitorRelayFetcher` objects to filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ public async byMonitorTags( monitorTags: RelayMonitorDiscoveryFilters, filter?: NDKFilter ): Promise {
+ const _filter: NDKFilter = { ...filter, ...monitorTags };
+ const events: RelayMonitorSetExt = await this.fetchMonitors(_filter);
+ return new Set(events) as RelayMonitorSetExt;
+ }
+
+ /**
+ * @description Fetches monitors and sorts by distance with a given geohash
+ *
+ * @param {string} geohash The geohash that represents the location to search for relays.
+ * @param {number} maxPrecision The maximum precision of the geohash to search for.
+ * @param {number} minPrecision The minimum precision of the geohash to search for.
+ * @param {number} minResults The minimum number of results to return.
+ * @param {boolean} recurse Recusively search for relays until results >= minResults
+ * @param {boolean} activeOnly Filter out inactive monitors.
+ * @param {NDKFilter} filter An optional, additional filter to ammend to the default filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ public async nearby( geohash: string, maxPrecision: number = 5, minPrecision: number = 5, minResults: number = 5, recurse: boolean = false, activeOnly: boolean = false, filter?: NDKFilter ): Promise {
+ if(!this.ndk){
+ return undefined;
+ }
+ let cb = async (evs: Set) => evs;
+ if(activeOnly){
+ cb = async (events: Set) => await MonitorFetcher.filterActiveMonitors(events as RelayMonitorSetExt) || new Set();
+ }
+ const kinds: NDKKind[] = [ NDKKind.RelayMonitor ];
+ const _filter: NDKFilter = { ...filter, kinds };
+ const geocodedEvents = await EventGeoCoded.fetchNearby(this.ndk, geohash, _filter, { maxPrecision, minPrecision, minResults, recurse, callbackFilter: cb });
+ const events: RelayMonitorSetExt= new Set(Array.from(geocodedEvents || new Set()).map( (event: EventGeoCoded) => (event as MonitorRelayFetcher) ));
+ return events;
+ }
+
+ /**
+ * @description Filters monitors by their active state
+ *
+ * @param {RelayMonitorSetExt} monitors A set of `MonitorRelayFetcher` objects to filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ static async filterActiveMonitors( monitors: RelayMonitorSetExt): Promise {
+ if(!monitors?.size) return undefined;
+ // const _monitors: RelayMonitorSetExt = new Set(Array.from(monitors)); //deref
+ const promises = [];
+ const activeMonitors: RelayMonitorSetExt = new Set();
+ for ( const $monitor of monitors) {
+ const active = await $monitor.isMonitorActive()
+ if(active){
+ $monitor.active = true;
+ activeMonitors.add($monitor);
+ }
+ }
+ return activeMonitors;
+ }
+
+ private async fetchMonitors(filter?: NDKFilter): Promise {
+ if(!this.ndk){
+ return undefined;
+ }
+
+ const kinds: NDKKind[] = [ NDKKind.RelayMonitor ];
+ const _filter: NDKFilter = { ...filter, kinds };
+ const ndkEvents = await this.ndk.fetchEvents(_filter);
+ let events: RelayMonitorSetExt = new Set();
+ events = castSetRelayMonitorFetchers(ndkEvents);
+ // await this.checkActive(events)
+ return events
+ }
+
+ private async checkActive(monitors: RelayMonitorSetExt){
+ const promises: Promise[] = [];
+ monitors?.forEach( (monitor: MonitorRelayFetcher) => {
+ promises.push(monitor.isMonitorActive())
+ })
+ await Promise.allSettled(promises)
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/src/fetchers/monitor-manager.test.ts b/libraries/kit/src/fetchers/monitor-manager.test.ts
new file mode 100644
index 00000000..a8656145
--- /dev/null
+++ b/libraries/kit/src/fetchers/monitor-manager.test.ts
@@ -0,0 +1,100 @@
+import { describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
+
+import NDK, { NDKEventGeoCoded } from '@nostr-dev-kit/ndk';
+import { MonitorManager } from './monitor-manager';
+
+// Mock NDKEventGeoCoded and related methods
+vi.mock('../../events/kinds/nip66/NDKEventGeoCoded', () => {
+ return {
+ NDKEventGeoCoded: vi.fn().mockImplementation(() => {
+ return {
+ fetchNearby: vi.fn(),
+ sortGeospatial: vi.fn()
+ };
+ }),
+ };
+});
+
+describe("MonitorManager", () => {
+ let ndk: NDK;
+ let options;
+ let MonitorManager;
+
+ beforeEach(() => {
+ ndk = new NDK();
+ options = {};
+ MonitorManager = new MonitorManager(ndk, options);
+ });
+
+ it("should be instantiated with the provided NDK instance", () => {
+ expect(MonitorManager.ndk).toBe(ndk);
+ });
+
+ it("should initialize monitors when populate is called", async () => {
+ const mockFetchEvents = vi.fn().mockResolvedValue(new Set());
+ ndk.fetchEvents = mockFetchEvents;
+
+ await MonitorManager.populate();
+
+ expect(mockFetchEvents).toHaveBeenCalled();
+ });
+
+ describe("Filtering and Sorting", () => {
+ it("should filter active monitors correctly", async () => {
+ const activeMonitor = { active: true };
+ const inactiveMonitor = { active: false };
+ const monitors = new Set([activeMonitor, inactiveMonitor]);
+
+ monitors.forEach(monitor => {
+ monitor.active = vi.fn().mockResolvedValue(monitor.active);
+ });
+
+ const filteredMonitors = await MonitorManager.filterActiveMonitors(monitors);
+
+ expect(filteredMonitors).toContain(activeMonitor);
+ expect(filteredMonitors).not.toContain(inactiveMonitor);
+ });
+
+ it("should sort monitors by proximity correctly", async () => {
+
+ const coords = { lat: 0, lon: 0 };
+ const monitors = new Set([{ lat: 1, lon: 1 }, { lat: -1, lon: -1 }]);
+
+ const sortedMonitors = await MonitorManager.sortMonitorsByProximity(coords, monitors);
+ });
+ });
+
+ describe('populateByCriterias', () => {
+ it('should populate monitors based on given criterias', async () => {
+ // Setup mocks for NDK methods used within populateByCriterias
+ const mockFetchMonitors = vi.fn();
+ ndk.fetchEvents = mockFetchMonitors.mockResolvedValue(new Set([/* Mock MonitorManager data */]));
+
+ const criterias = {/* Define criterias */};
+ await MonitorManager.populateByCriterias(criterias, true);
+
+ // Verify fetchEvents was called with expected filter
+ expect(mockFetchMonitors).toHaveBeenCalledWith(expect.objectContaining({
+ /* Expected filter derived from criterias */
+ }));
+
+ // Further assertions on the state of MonitorManager after populateByCriterias
+ expect(MonitorManager.monitors.size).toBeGreaterThan(0);
+ // Add more assertions as needed
+ });
+ });
+
+ describe('aggregate', () => {
+ it('should aggregate data based on the specified fetchAggregate method', async () => {
+ // const mockFetchOnlineRelays = vi.fn();
+ // NDKEventGeoCoded.prototype.fetchOnlineRelays = mockFetchOnlineRelays.mockResolvedValue(/* Mock data */);
+
+ // const fetchAggregate = 'onlineList'; // Example aggregate method
+ // const results = await MonitorManager.aggregate(fetchAggregate, options);
+
+ // expect(mockFetchOnlineRelays).toHaveBeenCalled();
+
+ // expect(results).toBeDefined();
+ });
+ });
+});
diff --git a/libraries/kit/src/fetchers/monitor-manager.ts b/libraries/kit/src/fetchers/monitor-manager.ts
new file mode 100644
index 00000000..3ff23adb
--- /dev/null
+++ b/libraries/kit/src/fetchers/monitor-manager.ts
@@ -0,0 +1,427 @@
+
+import type NDK from '@nostr-dev-kit/ndk';
+import type { NDKFilter, NDKSubscription, NDKRelayMonitor } from '@nostr-dev-kit/ndk';
+
+import { NDKEventGeoCoded as EventGeoCoded, NDKRelayMeta } from '@nostr-dev-kit/ndk';
+import type { Coords } from '@nostr-dev-kit/ndk';
+
+import { MonitorRelayFetcher, RelayMonitorDiscoveryTags } from './monitor-relay-fetcher';
+import type { RelayMonitorSetExt, RelayListSet, RelayMonitorDiscoveryFilters, RelayMonitorCriterias, RelayMetaSubscriptionHandlers, LoadRelayStat, LoadRelayStats } from './monitor-relay-fetcher';
+
+import { KitCacheRelayMonitorInterface, KitCacheRelayMonitorDefault, KitCacheRelayMetaDefault, KitCacheRelayMetaInterface } from '../cache/index';
+
+import { MonitorFetcher, MonitorFetcherOptions, MonitorFetcherOptionsDefaults } from './monitor-fetcher';
+
+type RelayAggregateMixed = RelayListSet | Set | undefined;
+
+type MonitorManagerOptions = {
+ primaryMonitor?: string;
+ monitorCache?: KitCacheRelayMonitorInterface;
+ relayCache?: KitCacheRelayMetaInterface;
+}
+
+type MonitorSubs = Map;
+
+/**
+ * @class MonitorManager
+ */
+export class MonitorManager {
+
+ private _ndk: NDK;
+ private _fetcher: MonitorFetcher;
+ private _monitorCache: KitCacheRelayMonitorInterface;
+ private _relayCache: KitCacheRelayMetaInterface;
+ private _relayFetcherFilters: Record;
+ private _primaryMonitor: string | undefined;
+
+ constructor( ndk: NDK, options?: MonitorManagerOptions, monitorFetcherOptions: MonitorFetcherOptions = MonitorFetcherOptionsDefaults, relayFetcherFilters: Record = {} ) {
+ this._ndk = ndk;
+ this._fetcher = new MonitorFetcher(this.ndk, monitorFetcherOptions || {} as MonitorFetcherOptions)
+ this._relayFetcherFilters = relayFetcherFilters
+ this._primaryMonitor = options?.primaryMonitor || undefined;
+ this._monitorCache = options?.monitorCache || new KitCacheRelayMonitorDefault();
+ this._relayCache = options?.relayCache || new KitCacheRelayMetaDefault();
+ }
+
+ get ndk(): NDK {
+ return this._ndk;
+ }
+
+ get fetch(): MonitorFetcher {
+ return this._fetcher;
+ }
+
+ set fetchOptions( options: MonitorFetcherOptions ) {
+ this.fetch.options = options;
+ }
+
+ set monitor( monitor: MonitorRelayFetcher ) {
+ this.cache.set( monitor );
+ }
+
+ get monitors(): Promise {
+ return new Promise(async (resolve) => {
+ const monitors = await this.cache.dump()
+ console.log('dump', monitors)
+ const promises = []
+ monitors?.forEach(monitor => {
+ monitor.cache = this._relayCache;
+ promises.push(monitor.init());
+ })
+ Promise.allSettled(promises).then(() => {
+ resolve(this.sortMonitorsByPrimary( monitors ));
+ })
+ });
+ }
+
+ get monitorKeys(): Promise> {
+ return this.cache.keys();
+ }
+
+ set monitors( monitors: RelayMonitorSetExt) {
+ if(!monitors?.size) return;
+ this.cache.reset();
+ this.cache.load( monitors )
+ }
+
+ get cache(): KitCacheRelayMonitorInterface {
+ return this._monitorCache;
+ }
+
+ set cache( cache: KitCacheRelayMonitorInterface ) {
+ this._monitorCache = cache;
+ }
+
+ resetMonitors(): void {
+ this.cache.reset();
+ }
+
+ async getMonitor( key: string ): Promise {
+ const monitor = await this.cache.get( key );
+ if(!monitor) return undefined
+ monitor.cache = this._relayCache
+ return monitor
+ }
+
+ async abortAll(): Promise {
+ (await this.monitors)?.forEach( monitor => {
+ monitor.abort()
+ })
+ }
+
+ async init(): Promise {
+ const activeMonitors: RelayMonitorSetExt = await this.fetch.monitors(undefined, false)
+ const sortedMonitors = await this.sortMonitorsByData(activeMonitors)
+ await this.initMonitors(sortedMonitors)
+ await this.populateMonitors(sortedMonitors)
+ // return this.populateRelays(sortedMonitors)
+ }
+
+ async fetchMonitors( filter?: NDKFilter, force?: boolean ): Promise {
+ return this.fetch.monitors(filter, force)
+ }
+
+ async initMonitors( monitors: RelayMonitorSetExt ): Promise {
+ if(!monitors) return
+ const promises: Promise[] = []
+ monitors.forEach( async (monitor: MonitorRelayFetcher) => {
+ promises.push(monitor.init(this._relayCache))
+ })
+ await Promise.allSettled(promises)
+ }
+
+ // async updateMonitorStat(monitorPubkey: string, stat: LoadRelayStat): Promise {
+ // return this.cache.db.where({monitorPubkey}).modify(stat)
+ // }
+
+ // async updateMonitorsStats( stats: Map ): Promise {
+ // const promises: Promise[] = []
+ // const it = stats.keys()
+ // for(let i=0; i {
+ console.log('found monitors', monitors?.size)
+ if(!monitors?.size) return;
+ await this.cache.load( monitors );
+ console.log('dumped monitors', await this.cache.dump())
+ }
+
+ async populateRelays( _monitors?: RelayMonitorSetExt ): Promise {
+ const monitors = _monitors || await this.monitors;
+ const stats: LoadRelayStats = new Map()
+ if (monitors) {
+ for await (const monitor of monitors) {
+ console.log('populating relays for ', monitor.pubkey)
+ stats.set(monitor.pubkey, await monitor.load(undefined, this._relayFetcherFilters?.[monitor.pubkey]));
+ }
+ }
+ return stats
+ }
+
+ async populateRelaysParallel(): Promise {
+ const promises: Promise[] = new Array();
+ const monitors = await this.monitors;
+ const stats: LoadRelayStats = new Map()
+ monitors?.forEach( async (monitor) => {
+ stats.set(monitor.pubkey, await monitor.load());
+ })
+ return stats
+ }
+
+ async subscribeToMonitor(pubkey: string, filter?: NDKFilter, callbacks?: RelayMetaSubscriptionHandlers): Promise {
+ const monitor = await this.cache.get(pubkey)
+ if(!monitor) return
+ return monitor.subscribeRelayMeta(callbacks, filter)
+ }
+
+ async subscribeToMonitors(callbacks?: RelayMetaSubscriptionHandlers): Promise {
+ const monitorFeeds: MonitorSubs = new Map();
+ const monitors = await this.monitors
+ console.log(monitors)
+ if(!monitors) return
+ Array.from(monitors).forEach( monitor => {
+ const sub = monitor.subscribeRelayMeta(callbacks)
+ if(!sub) return
+ monitorFeeds.set(monitor.pubkey, sub)
+ })
+ return monitorFeeds
+ }
+
+ async sortMonitorsByPrimary(monitors: RelayMonitorSetExt): Promise {
+ if (!monitors) return undefined;
+ let monitorsArray: NDKRelayMonitor[] = Array.from(monitors);
+ const primary = monitorsArray.find(monitor => monitor.pubkey === this._primaryMonitor);
+ monitorsArray.unshift(primary)
+ return new Set(monitorsArray) as RelayMonitorSetExt;
+ }
+
+ async sortMonitorsByData(monitors: RelayMonitorSetExt): Promise {
+ if (!monitors) return undefined;
+ const monitorsArray: NDKRelayMonitor[] = Array.from(monitors);
+ monitorsArray.sort((a, b) => b.checks.length - a.checks.length);
+ return new Set(monitorsArray) as RelayMonitorSetExt;
+ }
+
+ /**
+ * @description Aggregates relay data based on the specified fetch method and options.
+ * This method collects data from each monitor that meets the specified criteria
+ * and aggregates it based on the `fetchAggregate` parameter.
+ *
+ * @param fetchAggregate - The aggregation method to be used for fetching data.
+ * @param opts - Optional parameters including custom filters, criteria for monitor selection, and geospatial options for nearby search.
+ * @returns A promise that resolves to a mixed set of relay data based on the specified aggregation method.
+ *
+ * @todo fetchAggregate -> enum
+ * @public
+ * @async
+ */
+ async aggregate(fetchAggregate: string, opts?: MonitorFetcherOptions): Promise {
+ const promises: Promise[] = [];
+ const criterias = opts?.criterias || this.fetch.options?.criterias as RelayMonitorCriterias || undefined;
+ const monitors: RelayMonitorSetExt = criterias ? await this.meetsCriterias(criterias) : await this.cache.dump();
+
+ if (!monitors || monitors.size === 0) return undefined;
+
+ monitors.forEach( (monitor: MonitorRelayFetcher) => {
+ let result: Promise = Promise.resolve(undefined);
+ switch (fetchAggregate) {
+ case 'onlineList':
+ result = monitor.fetchOnlineRelays(opts?.customFilter);
+ break;
+ case 'onlineMeta':
+ result = monitor.fetchOnlineRelaysMeta(opts?.customFilter);
+ break;
+ case 'onlineListNearby':
+ if (!opts?.nearby) break;
+ result = monitor.fetchNearbyRelaysList(
+ opts?.nearby.geohash,
+ opts?.nearby?.maxPrecision,
+ opts?.nearby?.minPrecision,
+ opts?.nearby?.minResults,
+ opts?.nearby?.recurse,
+ opts?.customFilter
+ );
+ break;
+ }
+ promises.push(result);
+ });
+
+ const settledPromises = await Promise.allSettled(promises);
+
+ const results = new Set();
+ settledPromises.forEach((settled) => {
+ if (settled.status === 'fulfilled' && settled.value) {
+ results.add(settled.value);
+ }
+ });
+
+ return results as unknown as RelayAggregateMixed; // Cast to the appropriate type
+ }
+
+ /**
+ * @description Populates the internal set of monitors based on a custom filter and optionally filters for only active monitors.
+ * This method fetches relay monitors matching the provided filter and updates the internal set of monitors.
+ *
+ * @param customFilter - A custom filter to apply when fetching monitors.
+ * @param activeOnly - If true, only active monitors are considered.
+ * @returns A promise that resolves once the internal set of monitors is populated.
+ *
+ * @async
+ */
+ public async populate( customFilter: NDKFilter = {}, activeOnly: boolean = true ) {
+ this.cache.reset();
+ const events: RelayMonitorSetExt = await this.fetch.monitors(customFilter, activeOnly);
+ if(!events?.size) return undefined;
+ await this.cache.load(events)
+ await this._initMonitors();
+ }
+
+ /**
+ * @description Populates the internal set of monitors based on specified criteria and optionally filters for only active monitors.
+ * This method constructs a filter from the given criteria and fetches relay monitors that meet these criteria.
+ *
+ * @param criterias - Criteria used to filter the monitors.
+ * @param activeOnly - If true, only active monitors are considered.
+ * @returns A promise that resolves once the internal set of monitors is populated based on the criteria.
+ *
+ * @async
+ */
+ public async populateByCriterias( criterias: RelayMonitorCriterias, activeOnly: boolean = true ) {
+ const filter: NDKFilter = this._generateCriteriasFilter(criterias);
+ this.populate( filter, activeOnly );
+ }
+
+ /**
+ * @description Populates the internal set of monitors based on proximity to a given geohash and optionally appends a custom filter.
+ * This method fetches relay monitors that are nearby the specified geohash and meets any additional specified criteria.
+ *
+ * @param geohash - The geohash representing the location to search near.
+ * @param maxPrecision - The maximum precision of the geohash to consider.
+ * @param minPrecision - The minimum precision of the geohash to consider.
+ * @param minResults - The minimum number of results to return.
+ * @param recurse - If true, recursively search for relays until the minimum number of results is met.
+ * @param appendFilter - An optional filter to append to the default filter.
+ * @param activeOnly - If true, only considers active monitors.
+ * @returns A promise that resolves once the internal set of monitors is populated based on proximity.
+ * @public
+ * @async
+ */
+ public async populateNearby( geohash: string, maxPrecision: number = 5, minPrecision: number = 5, minResults: number = 5, recurse: boolean = false, appendFilter?: NDKFilter, activeOnly: boolean = false ) {
+ this.cache.reset();
+ const _builtinFilter: NDKFilter = this._generateCriteriasFilter();
+ const events: RelayMonitorSetExt = await this.fetch.nearby(geohash, maxPrecision, minPrecision, minResults, recurse, activeOnly, { ..._builtinFilter, ...appendFilter });
+ if(!events?.size) return undefined;
+ this.cache.load(events)
+ this._initMonitors();
+ }
+
+ /**
+ * @description Filters the internal set of monitors based on the specified criteria.
+ *
+ * @param criterias - The criteria used to filter the monitors.
+ * @returns A set of relay monitors that meet the specified criteria or undefined if no monitors meet the criteria.
+ * @public
+ */
+ public async meetsCriterias( criterias: RelayMonitorCriterias ): Promise {
+ let monitors = await this.cache.dump()
+ if(!monitors?.size) return undefined;
+ return new Set(Array.from(monitors).filter( (monitor: MonitorRelayFetcher) => monitor.meetsCriterias(criterias) ));
+ }
+
+ /**
+ * @description Retrieves the closest monitor to the specified coordinates that meets any provided criteria.
+ * If no monitors meet the criteria or are close enough, returns undefined.
+ *
+ * @param coords - The coordinates used to find the closest monitor.
+ * @param criterias - Optional criteria to filter monitors.
+ * @param populate - If true, populates the internal set of monitors based on the criteria before searching.
+ * @returns A promise that resolves to the closest monitor meeting the criteria, or undefined if no suitable monitor is found.
+ * @public
+ * @async
+ */
+ public async getClosestMonitor( coords: Coords, criterias?: RelayMonitorCriterias, populate: boolean = false ): Promise {
+ const _criterias = criterias || this.fetch.options?.criterias || {} as RelayMonitorCriterias;
+ let monitors = await this.monitors
+ if(!monitors?.size || populate) {
+ await this.populateByCriterias( criterias || this?.fetch.options?.criterias as RelayMonitorCriterias);
+ }
+ monitors = await this.meetsCriterias(_criterias);
+ if(!monitors?.size) return undefined;
+ const sorted: RelayMonitorSetExt = MonitorManager.sortMonitorsByProximity(coords, monitors);
+ return sorted?.values().next().value;
+ };
+
+ /**
+ * @description Sorts monitors based on provided coordinates (DD or geohash) relative to the monitor's coordinates (if available)
+ *
+ * @param {Coords} coords The coordinates to use for sorting.
+ * @param {RelayMonitorSetExt} monitors A set of `MonitorRelayFetcher` objects to filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @static
+ * @async
+ */
+ static sortMonitorsByProximity( coords: Coords, monitors: RelayMonitorSetExt ): RelayMonitorSetExt | undefined {
+ if(!monitors?.size) return undefined;
+ const monitorsSorted = EventGeoCoded.sortGeospatial( coords, monitors as Set );
+ return monitorsSorted as RelayMonitorSetExt;
+ }
+
+ /**
+ * @description Initializes monitors by calling their `init` method if they have not been initialized yet.
+ * This method iterates through all monitors and initializes each that hasn't been initialized.
+ * The initialization process for each monitor is performed asynchronously, and this method
+ * waits for all initialization promises to settle before completing.
+ *
+ * @private
+ * @async
+ */
+ private async _initMonitors(){
+ const promises: Promise[] = [];
+ let monitors = await this.monitors
+ if(!monitors?.size) return;
+ monitors?.forEach( async (monitor: MonitorRelayFetcher) => {
+ if(!monitor.initialized){
+ promises.push(monitor.init());
+ }
+ });
+ await Promise.allSettled(promises);
+ }
+
+ /**
+ * @description Generates a filter for relay monitor discovery based on specified criteria.
+ * The method maps the provided criteria to their corresponding discovery tags
+ * and constructs a filter object that can be used for relay monitor discovery.
+ * If no criteria are provided, it defaults to using the criteria specified in
+ * the instance's options, if available.
+ *
+ * @param criterias - Optional. The criteria to generate the filter from.
+ * @returns An object representing the filter for relay monitor discovery.
+ *
+ * @private
+ */
+ private _generateCriteriasFilter( criterias?: RelayMonitorCriterias ): RelayMonitorDiscoveryFilters {
+ const filter: RelayMonitorDiscoveryFilters = {};
+ criterias = criterias || this.fetch.options?.criterias;
+ if (!criterias) return filter;
+ const keyMapping: Record = {
+ kinds: RelayMonitorDiscoveryTags.kinds,
+ checks: RelayMonitorDiscoveryTags.checks,
+ };
+ Object.entries(keyMapping).forEach(([optionKey, tagValue]) => {
+ const filterKey = `#${tagValue}` as keyof RelayMonitorDiscoveryFilters;
+ const originalValue = criterias?.[optionKey as keyof RelayMonitorCriterias];
+ if (originalValue) {
+ filter[filterKey] = originalValue.map(String);
+ }
+ });
+ return filter;
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/src/fetchers/monitor-relay-fetcher.test.ts b/libraries/kit/src/fetchers/monitor-relay-fetcher.test.ts
new file mode 100644
index 00000000..a9b2cf37
--- /dev/null
+++ b/libraries/kit/src/fetchers/monitor-relay-fetcher.test.ts
@@ -0,0 +1,244 @@
+// import { describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
+
+// import NDK, { NDKKind, NDKRelayDiscovery, NDKRelayMeta } from '@nostr-dev-kit/ndk';
+// import type { NDKFilter } from '@nostr-dev-kit/ndk';
+
+// import type { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
+// import { MonitorRelayFetcher } from './monitor-relay-fetcher.js';
+
+// const EVENT_10166 = {"id":"dcc56a6b1b38ab4372bb95a9095a16406f501ae6abb17763a31bc2c4a33f2964","kind":10166,"pubkey":"abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832","created_at":1710936428,"content":"","tags":[["frequency","3600"],["o","9bbabc5e36297b6f7d15dd21b90ef85b2f1cb80e15c37fcc0c7f6c05acfd0019"],["k","30066"],["k","30166"],["c","open"],["c","read"],["c","write"],["c","info"],["c","dns"],["c","geo"],["c","ssl"],["timeout","open","30000"],["timeout","read","30000"],["timeout","write","30000"],["G","geohash"],["g","u0yjjee20","geohash"],["g","u0yjjee2","geohash"],["g","u0yjjee","geohash"],["g","u0yjje","geohash"],["g","u0yjj","geohash"],["g","u0yj","geohash"],["g","u0y","geohash"],["g","u0","geohash"],["g","u","geohash"],["G","countryCode"],["g","DE","countryCode"],["g","DEU","countryCode"],["G","countryName"],["g","Germany","countryName"],["G","cityName"],["g","Frankfurt am Main","cityName"]],"sig":"fabe5d4019e8bc4f9c9940bccc13fe07cae5eeb3fb3d03dd11756e35871558cf301c053a71db7f5828a7f41b3479514b6ca7cea658159db3eae7a3544db89f36"};
+// const EVENT_30166_A: NostrEvent = {"id":"0b44170d80ae592345f87f49b466d2f3391f53f4bb954e0cf95a65eae9242281","kind":30166,"pubkey":"abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832","created_at":1709611188,"content":"","tags":[["d","wss://relay.weloveit.info/"],["n","clearnet"],["N","0"],["N","1"],["N","2"],["N","3"],["N","4"],["N","5"],["N","6"],["N","7"],["N","8"],["N","9"],["N","10"],["N","11"],["R","!auth"],["R","!payment"],["s","git+https://github.com/hoytech/strfry.git"],["G","geohash"],["g","u1422u57b","geohash"],["g","u1422u57","geohash"],["g","u1422u5","geohash"],["g","u1422u","geohash"],["g","u1422","geohash"],["g","u142","geohash"],["g","u14","geohash"],["g","u1","geohash"],["g","u","geohash"],["G","countryCode"],["g","FR","countryCode"],["g","FRA","countryCode"],["G","countryName"],["g","France","countryName"],["G","regionCode"],["g","FR-HDF","regionCode"]],"sig":"531e6339e1a5df2761f5e9eb0c12d6b0aa9273c522aba3ab164dcbe802d53e553acf454f5790932ebaa87004b02aee45d10354e9fda153f5f539f98211c38975"};
+// const EVENT_30166_B: NostrEvent = {"id":"ade1b3b33b9a0bf98ce851a343d99bab39fa0491cf341b2b622d7f70786ff071","kind":30166,"pubkey":"abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832","created_at":1709769876,"content":"","tags":[["d","wss://africa.nostr.joburg/"],["n","clearnet"],["N","0"],["N","1"],["N","2"],["N","3"],["N","4"],["N","5"],["N","6"],["N","7"],["N","8"],["N","9"],["N","10"],["N","11"],["N","12"],["N","13"],["R","!auth"],["R","!payment"],["s","git+https://github.com/Cameri/nostream.git"],["G","geohash"],["g","ke7fy2z3v","geohash"],["g","ke7fy2z3","geohash"],["g","ke7fy2z","geohash"],["g","ke7fy2","geohash"],["g","ke7fy","geohash"],["g","ke7f","geohash"],["g","ke7","geohash"],["g","ke","geohash"],["g","k","geohash"],["G","countryCode"],["g","ZA","countryCode"],["g","ZAF","countryCode"],["G","countryName"],["g","South Africa","countryName"],["G","regionCode"],["g","ZA-GP","regionCode"]],"sig":"0a90ae93b3f3964072cd2eb30c949505980aa2f9df3c1c61912b41e84c0af67079e7df2719f3fc7d3914d8e8c81519228da50f3f71498ec5c323779c8c82a694"};
+// const EVENT_30066_A: NostrEvent = {"id":"e60efd752d5e86e7b0241db0cf0c11033ab8942a931d3f1704e9db55792b8756","kind":30066,"pubkey":"abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832","created_at":1709611187,"content":"{\"contact\":\"https://relay.weloveit.info\",\"description\":\"strfry relay , streaming to relayable and others , contact me for access.\",\"name\":\"relay.weloveit.info\",\"pubkey\":\"386058f50fb3ab679f9bcae74d731dea693874688d3064a504ef5f0fd5cdecb9\",\"software\":\"git+https://github.com/hoytech/strfry.git\",\"supported_nips\":[1,2,4,9,11,12,16,20,22,28,33,40],\"version\":\"0.9.6-7-g7196547\"}","tags":[["d","wss://relay.weloveit.info/"],["other","network","clearnet"],["rtt","open","142"],["rtt","read","83"],["rtt","write","113"],["nip11","name","relay.weloveit.info"],["nip11","desc","strfry relay , streaming to relayable and others , contact me for access."],["nip11","payment_required","false"],["nip11","auth_required","false"],["nip11","pubkey","386058f50fb3ab679f9bcae74d731dea693874688d3064a504ef5f0fd5cdecb9"],["nip11","contact","https://relay.weloveit.info"],["nip11","software","git+https://github.com/hoytech/strfry.git"],["nip11","version","0.9.6-7-g7196547"],["nip11","supported_nips","1","2","4","9","11","12","16","20","22","28","33","40"],["dns","ipv4","146.59.155.110"],["dns","as","AS16276 OVH SAS"],["dns","asname","OVH"],["dns","isp","OVH SAS"],["geo","lat","50.6916"],["geo","lon","3.20151"],["geo","tz","Europe/Paris"],["geo","cityName","Roubaix"],["geo","regionCode","FR-HDF"],["geo","regionName","Hauts-de-France"],["geo","countryName","France"],["geo","countryCode","FR"],["geo","geohash","u1422u57b"],["ssl","subject_alt_name","DNS:relay.weloveit.info"],["ssl","valid_from","1705363200"],["ssl","valid_to","1713225599"],["ssl","fingerprint","A9:7D:CA:AA:25:51:9E:A3:6E:16:E8:BB:3D:CC:00:2D:4D:C8:8D:BA"],["ssl","fingerprint256","BD:B2:F7:28:E1:2C:F7:09:EA:D6:75:E6:9E:11:C6:06:EA:DD:C2:C2:10:65:A8:32:73:C0:0A:0A:05:6F:87:BD"],["ssl","fingerprint512","FE:2F:8D:0D:97:51:D2:B6:C1:76:1C:79:C9:2D:57:17:30:FB:58:82:F7:93:D3:F9:A8:D7:AD:2F:8B:F4:F2:70:89:50:18:20:22:FC:71:07:E5:29:71:E4:D1:9F:76:8D:C9:60:82:A0:68:CA:6D:23:EE:73:80:56:1C:88:A5:32"],["ssl","ext_key_usage","1.3.6.1.5.5.7.3.1","1.3.6.1.5.5.7.3.2"],["ssl","serial_number","2E8151DE5B55B92EF908B1EF2FA18A37"],["ssl","pem_encoded","-----BEGIN CERTIFICATE-----\nMIIEBzCCA4ygAwIBAgIQLoFR3ltVuS75CLHvL6GKNzAKBggqhkjOPQQDAzBLMQsw\nCQYDVQQGEwJBVDEQMA4GA1UEChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBF\nQ0MgRG9tYWluIFNlY3VyZSBTaXRlIENBMB4XDTI0MDExNjAwMDAwMFoXDTI0MDQx\nNTIzNTk1OVowHjEcMBoGA1UEAxMTcmVsYXkud2Vsb3ZlaXQuaW5mbzBZMBMGByqG\nSM49AgEGCCqGSM49AwEHA0IABF04E1HPJ75PHu74Y2AA1bIZm99vcf6IpZwj88NS\nPOQwj2wmYN1inrOXbSngKOutVUj4/2/j8eMzCLRhWv7lgNmjggJ9MIICeTAfBgNV\nHSMEGDAWgBQPa+ZLzjlHrvZ+kB558DCRkshfozAdBgNVHQ4EFgQUXOC7H6t0mUqV\ntyx2NkiSyDO5lZQwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0l\nBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQEC\nAk4wJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM\nAQIBMIGIBggrBgEFBQcBAQR8MHowSwYIKwYBBQUHMAKGP2h0dHA6Ly96ZXJvc3Ns\nLmNydC5zZWN0aWdvLmNvbS9aZXJvU1NMRUNDRG9tYWluU2VjdXJlU2l0ZUNBLmNy\ndDArBggrBgEFBQcwAYYfaHR0cDovL3plcm9zc2wub2NzcC5zZWN0aWdvLmNvbTCC\nAQIGCisGAQQB1nkCBAIEgfMEgfAA7gB1AHb/iD8KtvuVUcJhzPWHujS0pM27Kdxo\nQgqf5mdMWjp0AAABjRSStXcAAAQDAEYwRAIgS01TIZYssKiTp+54o9LBjJDatHKS\npaL4A58h/PIxHTACIGwcFbcgRFI94PmwAc4JEoAwN2wSQGdip20Us2VRKTifAHUA\nO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcAAAGNFJK2NwAABAMARjBE\nAiASIS2oD1AhciolkakcfasDxiZusBIi4DlUIZcCaanlMwIgHXcenP539DY42R8T\nYuUMG38v7gmY4tXGq4e92Ui68zQwHgYDVR0RBBcwFYITcmVsYXkud2Vsb3ZlaXQu\naW5mbzAKBggqhkjOPQQDAwNpADBmAjEAyFjwjAhWlQZsEWBk30jiBjKhd16+/jr/\nXpKuWIWH44Ed1Dl1w5zb5likWLl1115nAjEAq9NuO9VV0GEjQx3htxpEmRvrRGpp\nD7rqgGDhNGwRWU7TqFr/lgdTB+OCf885ioiz\n-----END CERTIFICATE-----"]],"sig":"eade37a0867cbff922134cad6484ada02a1659123c343b7b9d6902d1d5a94eee8f619fdff4b8728a3a2dddb09871a566637958087d6ece531268a73ac68cddb2"};
+// const EVENT_30066_B: NostrEvent = {"id":"6ade1873b48669a5a0ae908f4b972be6daa581b8c09fb58d78b4e075b83ec206","kind":30066,"pubkey":"abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832","created_at":1709716068,"content":"{\"contact\":\"pastagringo@fractalized.net\",\"description\":\"Plebs are the white blood cells of the Bitcoin network, helping combat FUD (Fear, Uncertainty, Doubt) by being a vocal participant; helping educate and on-board no-coiners. Most plebs are Bitcoin maximalists, meaning they are proponents of supporting the adoption and growth of Bitcoin; not broader crypto. Bitcoin is secured by laser-eyed plebs. Few understand this.\",\"name\":\"relay.plebes.fans\",\"pubkey\":\"b12b632c887f0c871d140d37bcb6e7c1e1a80264d0b7de8255aa1951d9e1ff79\",\"software\":\"git+https://github.com/hoytech/strfry.git\",\"supported_nips\":[1,2,4,9,11,12,16,20,22,28,33,40],\"version\":\"0.9.6\"}","tags":[["d","wss://relay.plebes.fans/"],["other","network","clearnet"],["rtt","open","174"],["rtt","read","68"],["rtt","write","81"],["nip11","name","relay.plebes.fans"],["nip11","desc","Plebs are the white blood cells of the Bitcoin network, helping combat FUD (Fear, Uncertainty, Doubt) by being a vocal participant; helping educate and on-board no-coiners. Most plebs are Bitcoin maximalists, meaning they are proponents of supporting the adoption and growth of Bitcoin; not broader crypto. Bitcoin is secured by laser-eyed plebs. Few understand this."],["nip11","payment_required","false"],["nip11","auth_required","false"],["nip11","pubkey","b12b632c887f0c871d140d37bcb6e7c1e1a80264d0b7de8255aa1951d9e1ff79"],["nip11","contact","pastagringo@fractalized.net"],["nip11","software","git+https://github.com/hoytech/strfry.git"],["nip11","version","0.9.6"],["nip11","supported_nips","1","2","4","9","11","12","16","20","22","28","33","40"],["dns","ipv4","89.33.85.208"],["dns","as","AS199654 Oxide Group Limited"],["dns","asname","OXIDE-GROUP-LIMITED"],["dns","isp","Oxide Group Limited"],["dns","is_mobile","true"],["geo","lat","50.8955"],["geo","lon","6.06862"],["geo","tz","Europe/Amsterdam"],["geo","cityName","Eygelshoven"],["geo","regionCode","NL-LI"],["geo","regionName","Limburg"],["geo","countryName","The Netherlands"],["geo","countryCode","NL"],["geo","geohash","u1h3d15pc"],["ssl","subject_alt_name","DNS:*.plebes.fans, DNS:plebes.fans"],["ssl","valid_from","1708852049"],["ssl","valid_to","1716628048"],["ssl","fingerprint","11:E7:95:B1:D9:BD:2F:54:27:D4:F7:06:A5:E8:7E:EF:96:6C:68:D2"],["ssl","fingerprint256","BC:B5:4B:3C:16:5F:84:04:07:D2:B9:C8:72:4F:9D:28:EA:91:BF:28:ED:CE:E9:27:F2:F7:D4:76:61:C1:F1:B5"],["ssl","fingerprint512","56:37:D5:BC:71:02:89:FF:9F:41:3D:76:45:05:C6:10:4E:42:94:9A:6E:A9:3A:84:13:A3:B5:31:06:25:30:C9:19:1B:20:AB:34:E4:9B:E4:BF:25:17:C1:5F:A1:20:17:AA:DF:53:D2:61:02:F4:85:3F:05:68:61:D2:22:4E:F0"],["ssl","ext_key_usage","1.3.6.1.5.5.7.3.1","1.3.6.1.5.5.7.3.2"],["ssl","serial_number","03F4EA556F5EA3D1B34A02F13DB63981D7E8"],["ssl","pem_encoded","-----BEGIN CERTIFICATE-----\nMIIERjCCAy6gAwIBAgISA/TqVW9eo9GzSgLxPbY5gdfoMA0GCSqGSIb3DQEBCwUA\nMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD\nEwJSMzAeFw0yNDAyMjUwOTA3MjlaFw0yNDA1MjUwOTA3MjhaMBgxFjAUBgNVBAMM\nDSoucGxlYmVzLmZhbnMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATKMGI0DlBW92gv\njdsJ6F9tvp93u5Fr/xr/e8jtTqG77e01d+Uj0eUodbECdUiJEjp5CWw/+18aac3/\nYb1Ml5lxCwAPuFIyLn43NxLdUYLTrz9FWfkwIntFbs4bFs8lO4KjggIcMIICGDAO\nBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFCeOv1UaN9QfxKiRmpFBHMrgrwy3MB8GA1Ud\nIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggr\nBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRw\nOi8vcjMuaS5sZW5jci5vcmcvMCUGA1UdEQQeMByCDSoucGxlYmVzLmZhbnOCC3Bs\nZWJlcy5mYW5zMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBBAYKKwYBBAHWeQIEAgSB\n9QSB8gDwAHYAouK/1h7eLy8HoNZObTen3GVDsMa1LqLat4r4mm31F9gAAAGN37vm\n2AAABAMARzBFAiEA5MYKc2sU6MivaOHyM7h8lhdtq7sCjvFc892BSIgT50gCICB7\nWRQqV3KpDVrKqqxlv3u/QmeOsEb479xFcDP0NErGAHYA7s3QZNXbGs7FXLedtM0T\nojKHRny87N7DUUhZRnEftZsAAAGN37vmzgAABAMARzBFAiAH0TdER3FsIMrmKfZH\nBnzgCHy0Z1POZrpVZQSNy48ILwIhAL3xq0V4xCrCipONS6ugbkAvTAWAmyeTQIlr\nLfudeziFMA0GCSqGSIb3DQEBCwUAA4IBAQCkh0cvEV+PvrOj1i/j/rUJ6OMqEF8w\nxCn+l+4Ed1vKs8eDisZczNQoZLofzoXr7+cPCSA5ocdRErIVH8jMhqpsvVR09Kmn\n6SriIV+a8V+ZR7OaQE1/eymCmSWYvPG1+P6pYnmCB/wpitqIs4nFoSLLgdD+5Lne\nOaypNqgoGvV87hzxwoi9xLT3Kxj6JJ7m4UHwnwUfgTZ3W3uwbK70TiVlM7xIXBak\naZp1zbedD1Vvjz/QYDeVC3CS4cMXRQmDZQjH2wjxaNT5LaVHoVO2ftu4OnyJH73C\nheqtjv8Auxk5x50Q1KvYTkSPKgYopT8oV70OBmPtsGAwO3XVR6Okby+S\n-----END CERTIFICATE-----"]],"sig":"7a3cf106ca1be0f07b68b61f7e5bd47c93de9eeb71f643d36d6dd2a574dfde0f5e1a28bee1a355b5d9e4b44ba6244a364739af8039567d52689861f1e56be87f"};
+
+// let NDKEVENT_30066_1: NDKRelayMeta,
+// NDKEVENT_30066_2: NDKRelayMeta,
+// NDKEVENT_30166_1: NDKRelayDiscovery,
+// NDKEVENT_30166_2: NDKRelayDiscovery;
+
+// export const setupMetaEvents = (ndk: NDK): NDKRelayMeta[] => {
+// return [
+// new NDKRelayMeta(ndk, EVENT_30066_A),
+// new NDKRelayMeta(ndk, EVENT_30066_B)
+// ];
+// };
+
+// export const setupDiscoveryEvents = (ndk: NDK): NDKRelayDiscovery[] => {
+// return [
+// new NDKRelayDiscovery(ndk, EVENT_30166_A),
+// new NDKRelayDiscovery(ndk, EVENT_30166_B)
+// ];
+// };
+
+// export const testEventsMeta = [ EVENT_30066_A, EVENT_30066_B ];
+// export const testEventsDiscovery = [ EVENT_30166_A, EVENT_30166_B ];
+// export const testEvents = [ ...testEventsMeta, ...testEventsDiscovery ];
+
+// const explicitRelayUrls = ["wss://history.nostr.watch", "wss://purplepag.es"];
+
+// describe('MonitorRelayFetcher', () => {
+
+// let MonitorRelayFetcher: MonitorRelayFetcher;
+// const ndk = new NDK({ explicitRelayUrls });
+// [ NDKEVENT_30066_1 , NDKEVENT_30066_2 ] = setupMetaEvents(ndk);
+// [ NDKEVENT_30166_1, NDKEVENT_30166_2 ] = setupDiscoveryEvents(ndk);
+
+// const fetchEventsMock = vi.spyOn(ndk, "fetchEvents");
+// // const fetchEventMock = vi.spyOn(ndk, "fetchEvent");
+// const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+// beforeEach(() => {
+// MonitorRelayFetcher = new MonitorRelayFetcher(ndk, EVENT_10166);
+// });
+
+// describe('getters', ()=>{
+// it('should get pubkey', () => {
+// expect(MonitorRelayFetcher.pubkey).toBe("abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832");
+// });
+
+// it('should get frequency', () => {
+// expect(MonitorRelayFetcher.frequency).toBe(3600);
+// });
+
+// it('should get operator', () => {
+// expect(MonitorRelayFetcher.pubkey).toBe("9bbabc5e36297b6f7d15dd21b90ef85b2f1cb80e15c37fcc0c7f6c05acfd0019");
+// });
+
+// it('should get timeouts', () => {
+// expect(MonitorRelayFetcher.timeout).toEqual({
+// open: 30000,
+// read: 30000,
+// write: 30000
+// });
+// });
+
+// it('should get kinds', () => {
+// expect(MonitorRelayFetcher.kinds).toContain(NDKKind.RelayMeta);
+// expect(MonitorRelayFetcher.kinds).toContain(NDKKind.RelayDiscovery);
+// });
+
+// // it('should get geo', () => {
+// // expect(MonitorRelayFetcher.getGeo()).toEqual( expect.arrayContaining([["g", "u0yjjee20", "geohash"]]));
+// // });
+
+// });
+
+// describe('setters', ()=>{
+// it('should set pubkey', () => {
+// expect(MonitorRelayFetcher.pubkey).toBe("abcde937081142db0d50d29bf92792d4ee9b3d79a83c483453171a6004711832");
+// });
+
+// it('should set and get frequency correctly', () => {
+// expect(MonitorRelayFetcher.frequency).toBe(3600);
+// });
+
+// it('should set operator correctly', () => {
+// expect(MonitorRelayFetcher.pubkey).toBe("9bbabc5e36297b6f7d15dd21b90ef85b2f1cb80e15c37fcc0c7f6c05acfd0019");
+// });
+
+// it('should set timeouts correctly', () => {
+// expect(MonitorRelayFetcher.timeout).toEqual({
+// open: 30000,
+// read: 30000,
+// write: 30000
+// });
+// });
+
+// it('should set kinds correctly', () => {
+// // MonitorRelayFetcher.kinds = [NDKKind.RelayMeta, NDKKind.RelayDiscovery];
+// expect(MonitorRelayFetcher.kinds).toContain(NDKKind.RelayMeta);
+// expect(MonitorRelayFetcher.kinds).toContain(NDKKind.RelayDiscovery);
+// });
+
+// // it('should set timeouts correctly', () => {
+// // expect(MonitorRelayFetcher.getGeo()).toEqual( expect.arrayContaining([["g", "u0yjjee20", "geohash"]]));
+// // });
+// });
+
+// describe('@private', () => {
+// let MonitorRelayFetcher: MonitorRelayFetcher;
+
+// beforeEach(() => {
+// MonitorRelayFetcher = new MonitorRelayFetcher(ndk);
+// });
+
+// afterEach(() => {
+// vi.resetAllMocks();
+// });
+
+// describe('reduceRelayEventsToRelayStrings', () => {
+// it('should return a relay list', async () => {
+// const result = MonitorRelayFetcher['_reduceRelayEventsToRelayStrings'](new Set([NDKEVENT_30166_1, NDKEVENT_30166_2]) as Set);
+// expect(result).toBeInstanceOf(Set);
+// expect(result).toEqual(new Set(['wss://relay.weloveit.info/', 'wss://africa.nostr.joburg/']));
+// });
+// });
+
+// describe('invalidRelayFetch', () => {
+// it('should return undefined and log error', () => {
+// const result = MonitorRelayFetcher['_invalidRelayFetch']("testMethod", "testError");
+// expect(result).toBeUndefined();
+// expect(mockConsoleError).toHaveBeenCalledWith("testMethod: testError");
+// });
+// });
+
+// describe('nip66Filter', () => {
+// it('should generate correct filter based on input kinds', () => {
+// const filter = MonitorRelayFetcher['_nip66Filter']( [NDKKind.RelayMeta], { limit: 1 }, { "#n": ["clearnet"] } );
+// expect(filter).toHaveProperty('kinds', expect.arrayContaining([NDKKind.RelayMeta]));
+// expect(filter).toHaveProperty('limit', 1);
+// expect(filter).toHaveProperty('#n', expect.arrayContaining(["clearnet"]));
+// });
+// });
+// });
+
+// describe('@public', () => {
+
+// beforeEach(() => {
+// monitorRelayFetcher = new MonitorRelayFetcher(ndk, EVENT_10166);
+// fetchEventsMock.mockImplementation((filter): Promise> => {
+// let result: NDKEvent[] = [NDKEVENT_30066_1, NDKEVENT_30066_2, NDKEVENT_30166_1, NDKEVENT_30166_2];
+// filter = filter as NDKFilter;
+// const kinds = filter?.kinds?.map(kind => kind as number);
+// result = result.filter( event => kinds?.includes(event.kind as number) );
+// const f = filter as NDKFilter;
+// if (f instanceof Object) {
+// if (f?.["#s"]) {
+// result = result
+// .filter(event => 'software' in event && f?.["#s"]?.includes((event as any)?.software || ""));
+// }
+// if (f?.["#d"]) {
+// result = result.filter(event => 'url' in event && f?.["#d"]?.includes((event as any)?.url || ""));
+// }
+// }
+// return Promise.resolve(new Set(result));
+// });
+// });
+
+// describe('isMonitorValid',()=>{
+// it('should return true when all conditions are met', () => {
+// expect(monitorRelayFetcher.isMonitorValid()).toBe(true);
+// });
+
+// it('should return false when not all conditions are met', () => {
+// MonitorRelayFetcher.frequency = undefined;
+// expect(MonitorRelayFetcher.isMonitorValid()).toBe(false);
+// });
+// });
+
+// describe('fetchOnlineRelays', ()=>{
+// it('should return a set of relay events', async () => {
+// const result = await MonitorRelayFetcher['fetchOnlineRelays']();
+// expect(result).toBeInstanceOf(Set);
+// expect(result?.size).toBe(2);
+// expect(result).toEqual(new Set(['wss://relay.weloveit.info/', 'wss://africa.nostr.joburg/']));
+// });
+
+// it('should return a filtered set of relay events', async () => {
+// const filter: NDKFilter = { "#s": ['git+https://github.com/Cameri/nostream.git'] };
+// const result = await MonitorRelayFetcher.fetchOnlineRelays(filter);
+// expect(result).toBeInstanceOf(Set);
+// expect(result?.size).toBe(1);
+// expect(result).toEqual(new Set(['wss://africa.nostr.joburg/']));
+// });
+// });
+
+// describe('fetchRelayMeta', ()=>{
+// it('should return a NDKRelayMeta[] with length=1 when given one relay', async () => {
+// const result = await MonitorRelayFetcher.fetchRelayMeta('wss://relay.weloveit.info/');
+// const _result = Array.from(result || []);
+// expect(_result?.length).toEqual(1);
+// expect(_result?.[0]?.url).toEqual('wss://relay.weloveit.info/');
+// });
+
+// it('should return a NDKRelayMeta[] with length=2 given two relays', async () => {
+// const result = await MonitorRelayFetcher.fetchRelayMeta(['wss://relay.plebes.fans/', 'wss://relay.weloveit.info/']);
+// const _result = Array.from(result || []);
+// expect(_result?.length).toEqual(2);
+// expect(_result?.[0]?.url).toEqual('wss://relay.weloveit.info/');
+// expect(_result?.[1]?.url).toEqual('wss://relay.plebes.fans/');
+// });
+// });
+
+// describe('fetchOnlineRelaysMeta', ()=>{
+// it('should return a set of NDKRelayMeta events', async () => {
+// const result = await MonitorRelayFetcher.fetchOnlineRelaysMeta();
+// const _result = Array.from(result || []);
+// expect(_result?.length).toEqual(2);
+// expect(_result?.[0]?.url).toEqual('wss://relay.weloveit.info/');
+// expect(_result?.[1]?.url).toEqual('wss://relay.plebes.fans/');
+// });
+// });
+// });
+
+// describe('@static', () => {
+// describe('from', () => {
+// it('should return a MonitorRelayFetcher instance', () => {
+// const newRelayMonitor = MonitorRelayFetcher.from(NDKEVENT_30066_1);
+// expect(newRelayMonitor).toBeInstanceOf(MonitorRelayFetcher);
+// expect(newRelayMonitor?.checks).toBeDefined();
+// });
+// });
+// });
+// });
\ No newline at end of file
diff --git a/libraries/kit/src/fetchers/monitor-relay-fetcher.ts b/libraries/kit/src/fetchers/monitor-relay-fetcher.ts
new file mode 100644
index 00000000..a641db5c
--- /dev/null
+++ b/libraries/kit/src/fetchers/monitor-relay-fetcher.ts
@@ -0,0 +1,714 @@
+import import2 from "import2";
+import { is_node } from "tstl";
+if (is_node()) (globalThis as any).WebSocket ??= import2("ws");
+
+import NDK, {
+ NDKRelayList,
+ NDKRelay,
+ NDKEvent,
+ NDKKind,
+ NDKEventGeoCoded as EventGeoCoded,
+ NDKRelayMonitor, NDKRelayMeta, NDKRelayDiscovery,
+ RelayLiveness, RelayDiscoveryFilters, FetchNearbyRelayOptions
+} from "@nostr-dev-kit/ndk";
+
+import { } from "@nostr-dev-kit/ndk"
+
+import { getRelayListForUser } from "@nostr-dev-kit/ndk";
+
+import { NostrFetcher } from 'nostr-fetch';
+import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
+
+import type { NostrEvent, NDKFilter, NDKUserProfile, NDKUser, NDKSubscription } from "@nostr-dev-kit/ndk";
+import { KitCacheRelayMetaInterface, KitCacheRelayMetaDefault } from "../cache/index";
+
+import { popProp } from "../utils";
+
+export type RelayListSet = Set | undefined
+export type RelayMonitorSetExt = Set | undefined
+export type RelayDiscoveryResult = Set | undefined
+export type RelayMetaSet = Set | undefined
+
+export type LoadRelayStat = {
+ count: number;
+ mostRecent: number;
+}
+
+export type LoadRelayStats = Map
+
+export interface RelayMetaSubscriptionHandlers {
+ onEvent?: ( event: NDKEvent ) => {},
+ onEose?: ( sub: NDKSubscription ) => {},
+ onDup?: ( event: NDKEvent ) => {},
+ onClose?: ( sub: NDKSubscription ) => {}
+}
+
+export type RelayMonitorCriterias = {
+ kinds: number[],
+ checks: string[]
+}
+
+export enum RelayMonitorDiscoveryTags {
+ kinds = "k",
+ checks = "c"
+}
+
+export type RelayMonitorDiscoveryFilters = {
+ [K in RelayMonitorDiscoveryTags as `#${K}`]?: string[];
+};
+
+export type FetchRelaysOptions = {
+ filter?: NDKFilter;
+ indexedTags?: RelayDiscoveryFilters;
+ geohash?: string,
+ nearby?: FetchNearbyRelayOptions;
+ activeOnly?: boolean;
+ tolerance?: number;
+}
+
+/**
+ * A `MonitorRelayFetcher` event represents a NIP-66 Relay Monitor.
+ *
+ * @author sandwich.farm
+ * @extends EventGeoCoded
+ * @summary Relay Monitor (NIP-66)
+ * @implements NDKKind.MonitorRelayFetcher
+ * @example
+ * ```javascript
+ * import { NDK } from "@nostr-dev-kit/ndk";
+ * import { MonitorRelayFetcher } from "@nostr-dev-kit/ndk/dist/events/kinds/nip66/relay-monitor";
+ *
+ * const ndk = new NDK();
+ * const monitorEvent = {...}
+ * const monitor = new MonitorRelayFetcher(ndk, monitorEvent);
+ * const online = await monitor.fetchOnlineRelays();
+ *
+ * console.log(online)
+ * ```
+ */
+export class MonitorRelayFetcher extends NDKRelayMonitor {
+ private _initialized: boolean = false;
+ private _cache: KitCacheRelayMetaInterface;
+
+ private fetcher: NostrFetcher;
+ private _is_fetching: boolean = false
+ private abortController: AbortController
+ private abortSignal: AbortSignal;
+
+ public allow_concurrent_fetches: boolean = false;
+
+ protected _relays: NDKRelayList | undefined;
+ protected _profile: NDKUserProfile | undefined;
+
+ constructor( ndk: NDK | undefined, event?: NostrEvent, cache?: KitCacheRelayMetaInterface) {
+ super(ndk, event);
+ this.kind ??= NDKKind.RelayDiscovery;
+ this.ndk = ndk || MonitorRelayFetcher.newNDK()
+ this._cache = cache || new KitCacheRelayMetaDefault();
+ this.fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk as NDK));
+
+ this.abortController = new AbortController()
+ this.abortSignal = this.abortController.signal;
+ }
+
+ static from(event: MonitorRelayFetcher): MonitorRelayFetcher {
+ return new MonitorRelayFetcher(event.ndk, event.rawEvent());
+ }
+
+ static newNDK(): NDK {
+ //TODO: apply defaults, etc
+ return new NDK();
+ }
+
+ get initialized(): boolean {
+ return this._initialized;
+ }
+
+ get cache(): KitCacheRelayMetaInterface {
+ return this._cache;
+ }
+
+ set cache( cache: KitCacheRelayMetaInterface ) {
+ console.log('cache set:', cache.constructor.name);
+ this._cache = cache;
+ }
+
+ set profile(profile: NDKUserProfile) {
+ this._profile = profile
+ }
+
+ get profile(): NDKUserProfile | undefined {
+ return this._profile;
+ }
+
+ set relays(relays: NDKRelayList) {
+ this._relays = relays;
+ }
+
+ get relays(): NDKRelayList | undefined {
+ return this._relays;
+ }
+
+ async init(cache?: KitCacheRelayMetaInterface): Promise {
+ const promises: Promise[] = [];
+ if(cache) this.cache = cache;
+ promises.push(this.setMonitorProfile());
+ promises.push(this.setMonitorRelays());
+ await Promise.allSettled(promises);
+ this._initialized = true;
+ }
+
+ abort(){
+ this.abortController.abort();
+ }
+
+ public async set(relayMeta: NDKRelayMeta): Promise {
+ await this.cache.set(relayMeta)
+ }
+
+ public async get(relay: string, monitorPubkey?: string): Promise {
+ if(monitorPubkey) {
+ return this.cache.get(relay, monitorPubkey)
+ }
+ return this.cache.list(relay)
+ }
+
+ /**
+ * @description Populates cache with all known relays from monitor
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a LoadRelayStat including properties count and mostRecent (timestamp)
+ *
+ * @public
+ * @async
+ */
+ public async load(livenesses: RelayLiveness[] = [ RelayLiveness.Online ], customFilter: NDKFilter = {}): Promise {
+ let events: RelayMetaSet = new Set();
+ let mostRecent: number = 0
+ for (const liveness of livenesses) {
+ console.log(`${this.pubkey} Fetching events for liveness: ${liveness}`);
+ events = await this.fetchRelaysMeta(customFilter, liveness);
+ if(!events?.size) continue;
+ await this.cache.load(events);
+ }
+ mostRecent = Math.max(...Array.from(events || new Set()).map((e: NDKRelayMeta) => e.created_at || 0));
+ const count = events?.size || 0
+ return {
+ count,
+ mostRecent
+ }
+ }
+
+ /**
+ * @description Loads cache with all known relays from monitor
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a LoadRelayStat including properties count and mostRecent (timestamp)
+ *
+ * @public
+ * @async
+ */
+ public async loadAll( customFilter: NDKFilter = {} ): Promise {
+ return this.load([RelayLiveness.All], customFilter)
+ }
+
+ /**
+ * @description Helper to load cache with online relays
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelaysMeta}
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a LoadRelayStat including properties count and mostRecent (timestamp)
+ *
+ * @public
+ * @async
+ */
+ public async loadOnline( customFilter: NDKFilter = {} ): Promise {
+ return this.load(undefined, customFilter)
+ }
+
+ /**
+ * @description Helper to load cache with offline relays
+ *
+ * @see {@link MonitorRelayFetcher#fetchOfflineRelaysMeta}
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a LoadRelayStat including properties count and mostRecent (timestamp)
+ *
+ * @public
+ * @async
+ */
+ public async loadOffline( customFilter: NDKFilter = {} ): Promise {
+ return this.load([RelayLiveness.Offline], customFilter)
+ }
+
+ /**
+ * @description Helper to load cache with dead relays
+ *
+ * @see {@link MonitorRelayFetcher#fetchOfflineRelaysMeta}
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a LoadRelayStat including properties count and mostRecent (timestamp)
+ *
+ * @public
+ * @async
+ */
+ public async loadDead( customFilter: NDKFilter = {} ): Promise {
+ return this.load([RelayLiveness.Dead], customFilter)
+ }
+
+ /**
+ * @description Generic fetcher method for fetching lists of relays
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ fetchRelaysList(prependFilter?: NDKFilter, appendFilter?: NDKFilter, liveness?: RelayLiveness): Promise {
+ return new Promise((resolve, reject) => {
+ this.fetchRelayMetaEvents(prependFilter, appendFilter, liveness)
+ .then((events: Set) => {
+ const relayMetaEvents: Set = new Set(Array.from(events).map((event: NDKEvent) => NDKRelayMeta.from(event)));
+ resolve(this._reduceRelayEventsToRelayStrings(relayMetaEvents));
+ })
+ .catch(() => new Set())
+ });
+ }
+
+ /**
+ * @description Creates a subscription and accepts handlers
+ *
+ * @public
+ * @param callback Callbacks to handle subscription events
+ * @param filter Additional filters for subscription
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ subscribeRelayMeta( callback?: RelayMetaSubscriptionHandlers, filter?: NDKFilter ): NDKSubscription | undefined {
+ console.log('subscribe', this.pubkey)
+ if(!this?.ndk) {
+ console.warn('Was not instantiated with ndk instance, cannot subscribe')
+ return
+ }
+ const kinds: NDKKind[] = [this.kinds.includes(NDKKind.RelayDiscovery )? NDKKind.RelayDiscovery: NDKKind.RelayMeta];
+ const _filter: NDKFilter = this.nip66Filter(kinds, undefined, filter, RelayLiveness.Online);
+ const sub: NDKSubscription = this?.ndk.subscribe(_filter, { closeOnEose: false });
+ const wrapEventHandler = async ( event: NDKEvent, callback = (relayMeta: NDKRelayMeta) => {} ) => {
+ console.log(event.id)
+ const relayMeta: NDKRelayMeta = NDKRelayMeta.from(event);
+ await this.set(relayMeta)
+ callback(relayMeta)
+ }
+
+ sub.on("event", (event: NDKEvent) => { wrapEventHandler(event, callback?.onEvent) });
+ if(callback?.onEose) sub.on("eose", callback.onEose);
+ if(callback?.onClose) sub.on("close", callback.onClose);
+ sub.start();
+ return sub;
+ }
+
+ /**
+ * @description Fetches a list of all known relays from monitor
+ *
+ * @see {@link MonitorRelayFetcher#fetchRelaysList}
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ async fetchAllRelays( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysList(filter, undefined, RelayLiveness.All)
+ }
+
+ /**
+ * @description Fetches a list of online relays
+ *
+ * @see {@link MonitorRelayFetcher#fetchRelaysList}
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ async fetchOnlineRelays( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysList(filter, undefined, RelayLiveness.Online)
+ }
+
+ /**
+ * @description Fetches a list of offline relays
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ async fetchOfflineRelays( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysList(filter, undefined, RelayLiveness.Offline)
+ }
+
+ /**
+ * @description Fetches a list of dead relays
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ async fetchDeadRelays( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysList(filter, undefined, RelayLiveness.Dead)
+ }
+
+ /**
+ * @description Fetches metadata for a specific relay or relays. This method may not work if you
+ * provide more relays in the array than the relay storing the relay events allows for in a tag filter.
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelaysMeta}
+ * @param {string[] | string} relays A string or array of strings representing the relay(s) to fetch metadata for.
+ * @returns A promise that resolves to the `RelayMetasResult` object(s)
+ *
+ * @public
+ * @async
+ */
+ async fetchRelayMeta( relays: string[] | string ): Promise {
+ if(!Array.isArray(relays)) {
+ relays = [relays];
+ }
+ const filter: NDKFilter = { "#d": relays } as NDKFilter;
+ return this.fetchRelayMetaEvents(filter, undefined, RelayLiveness.Offline)
+ }
+
+ /**
+ * @description Generic fetcher method for fetching relay events.
+ *
+ * @public
+ * @param filter A filter to apply additional filtering to subscription.
+ * @returns Promise resolves to a relay list
+ *
+ * @public
+ * @async
+ */
+ fetchRelayMetaEvents(prependFilter?: NDKFilter, appendFilter?: NDKFilter, liveness?: RelayLiveness): Promise> {
+ this.maybeWarnInvalid();
+ if (!this.kinds.includes(NDKKind.RelayMeta)) {
+ return Promise.reject(this._invalidRelayFetch(`MonitorRelayFetcher.fetchRelaysMeta()`, `${this.pubkey} does not publish kind ${NDKKind.RelayMeta}`));
+ }
+
+ const kinds: NDKKind[] = [NDKKind.RelayMeta];
+ const filter: NDKFilter = this.nip66Filter(kinds, prependFilter, appendFilter, liveness);
+
+ const timeRange = popProp(filter, 'since', 'until');
+
+ return new Promise(async (resolve, reject) => {
+ if (!this.ndk?.explicitRelayUrls) return reject("explicitRelayUrls not set");
+
+ const events = await this.fetcher.fetchAllEvents(
+ this.ndk?.explicitRelayUrls,
+ filter,
+ timeRange,
+ { abortSignal: this.abortSignal }
+ );
+
+ try {
+ const relayMetaEvents: Set = new Set();
+ events.forEach((event: any) => {
+ relayMetaEvents.add(new NDKRelayMeta(this.ndk, event));
+ });
+ resolve(relayMetaEvents)
+ } catch (error) {
+ console.error(`${this.pubkey} fetchRelayMetaEvents() Error fetching relay meta events: ${error}`);
+ reject(error);
+ }
+ });
+ }
+ /**
+ * @description
+ *
+ * @see {@link MonitorRelayFetcher#fetchRelayMetaEvents}
+ * @param {string[] | string} relays A string or array of strings representing the relay(s) to fetch metadata for.
+ * @returns A promise that resolves to the `RelayMetasResult` object(s)
+ *
+ * @public
+ * @async
+ */
+ async fetchRelaysMeta(filter?: NDKFilter, liveness: RelayLiveness = RelayLiveness.Online): Promise {
+ return this.fetchRelayMetaEvents(filter, undefined, liveness);
+ }
+
+ /**
+ * @description Fetches metadata for all relays known by monitor, optionally applying an additional filter.
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelaysMeta}
+ * @param {NDKFilter} filter An optional `NDKFilter` object to apply additional filtering criteria.
+ * @returns A promise that resolves to a `RelayMetaSet` or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchAllRelaysMeta( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysMeta(filter, RelayLiveness.Online)
+ }
+
+ /**
+ * @description Fetches metadata for online relays, optionally applying an additional filter.
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelaysMeta}
+ * @param {NDKFilter} filter An optional `NDKFilter` object to apply additional filtering criteria.
+ * @returns A promise that resolves to a `RelayMetaSet` or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchOnlineRelaysMeta( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysMeta(filter, RelayLiveness.Online)
+ }
+
+ /**
+ * @description Fetches metadata for offline relays, optionally applying an additional filter.
+ *
+ * @param {NDKFilter} filter An optional `NDKFilter` object to apply additional filtering criteria.
+ * @returns A promise that resolves to a `RelayMetaSet` or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchOfflineRelaysMeta( filter?: NDKFilter ): Promise {
+ return this.fetchRelaysMeta(filter, RelayLiveness.Offline)
+ }
+
+ /**
+ * @description Fetches metadata for offline relays, optionally applying an additional filter.
+ *
+ * @param {NDKFilter} filter An optional `NDKFilter` object to apply additional filtering criteria.
+ * @returns A promise that resolves to a `RelayMetaSet` or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchDeadRelaysMeta( filter?: NDKFilter ): Promise {
+ return this.fetchRelayMetaEvents(filter, undefined, RelayLiveness.Dead)
+ }
+
+ /**
+ * @description Fetches a list of online relays by providing one or more NDKFilters using RelayDiscoveryFilters keys.
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelays}
+ * @param {RelayDiscoveryFilters} indexedTags A `RelayDiscoveryFilters` value representing the tag to filter by.
+ * @param {NDKFilter} filter A string or array of strings representing the key(s) to filter by.
+ * @returns {Promise} A promise that resolves to a list of online relays as strings or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchOnlineRelaysBy( indexedTags: RelayDiscoveryFilters, filter?: NDKFilter ): Promise {
+ this.maybeWarnInvalid();
+ if( ![NDKKind.RelayMeta, NDKKind.RelayDiscovery].some(value => this.kinds.includes(value)) ) {
+ return this._invalidRelayFetch(`MonitorRelayFetcher.fetchOnlineRelaysBy()`, `${this.pubkey} does not publish kind ${NDKKind.RelayMeta} or ${NDKKind.RelayDiscovery}`);
+ }
+
+ const kinds = [this.kinds.includes(NDKKind.RelayDiscovery )? NDKKind.RelayDiscovery: NDKKind.RelayMeta];
+ const _filter: NDKFilter = this.nip66Filter(kinds, filter, indexedTags as NDKFilter);
+
+ return new Promise((resolve, reject ) => {
+ this.fetchOnlineRelays(_filter)
+ .then( (events: RelayListSet) => {
+ resolve(events);
+ })
+ .catch(reject);
+ });
+ }
+
+ /**
+ * @description Fetches metadata for online relays by filtering a specific tag and key, optionally applying an additional filter.
+ *
+ * @see {@link MonitorRelayFetcher#fetchOnlineRelaysMeta}
+ * @param {RelayDiscoveryFilters} indexedTags A `RelayDiscoveryTags` value representing the tag to filter by.
+ * @param {NDKFilter} filter A string or array of strings representing the key(s) to filter by.
+ * @returns Promise resolves to an array of `NDKRelayMeta` objects.
+ *
+ * @public
+ * @async
+ */
+ async fetchOnlineRelaysMetaBy( indexedTags: RelayDiscoveryFilters, filter?: NDKFilter ): Promise {
+ const _filter = indexedTags as NDKFilter;
+ return new Promise((resolve, reject) => {
+ this.fetchOnlineRelaysMeta(_filter)
+ .then( ( events ) => {
+ resolve( events );
+ })
+ .catch(reject);
+ });
+ }
+
+ /**
+ * @description Fetches relay discovery events for online relays, optionally applying an additional filter.
+ *
+ *
+ * @param {NDKFilter} filter An optional `NDKFilter` object to apply additional filtering criteria.
+ * @returns Promise resolves to a `RelayMetaSet` or undefined if the operation fails.
+ *
+ * @public
+ * @async
+ */
+ async fetchOnlineRelaysDiscovery( filter?: NDKFilter ): Promise {
+ this.maybeWarnInvalid();
+ if(this._is_fetching && !this.allow_concurrent_fetches) return this._invalidRelayFetch(`MonitorRelayFetcher.fetchOnlineRelaysDiscovery()`, 'There are already ongoing fetches. Set `allow_concurrent_fetches` to true if you want to override this behavior.')
+ if( !this.kinds.includes(NDKKind.RelayDiscovery) ) {
+ return this._invalidRelayFetch(`MonitorRelayFetcher.fetchOnlineRelaysDiscovery()`, `${this.pubkey} does not publish kind ${NDKKind.RelayMeta}`);
+ }
+
+ const kinds: NDKKind[] = [NDKKind.RelayDiscovery];
+ filter = this.nip66Filter(kinds, filter);
+
+ const timeRange = popProp(filter, 'since', 'until');
+
+ return new Promise(async (resolve, reject) => {
+ this._is_fetching = true;
+ const it = this.fetcher.allEventsIterator(
+ [],
+ filter || {},
+ timeRange,
+ { abortSignal: this.abortSignal }
+ );
+ const relayDiscoveryEvents: Set = new Set();
+ for await (const event of it) {
+ relayDiscoveryEvents.add(new NDKRelayDiscovery(this.ndk, event))
+ }
+ this._is_fetching = false;
+ resolve(relayDiscoveryEvents);
+ })
+ }
+
+ /**
+ * @description Fetches relays and sorts by distance with a given geohash
+ *
+ * @param {string} geohash The geohash that represents the location to search for relays.
+ * @param {number} maxPrecision The maximum precision of the geohash to search for.
+ * @param {number} minPrecision The minimum precision of the geohash to search for.
+ * @param {number} minResults The minimum number of results to return.
+ * @param {boolean} recurse Recusively search for relays until results >= minResults
+ * @param {NDKFilter} filter An optional, additional filter to ammend to the default filter.
+ * @returns Promise resolves to an array of `RelayListSet` objects.
+ *
+ * @public
+ * @async
+ */
+ async fetchNearbyRelaysList( geohash: string, maxPrecision: number = 5, minPrecision: number = 5, minResults: number = 5, recurse: boolean = false, filter?: NDKFilter ): Promise {
+ this.maybeWarnInvalid();
+ if(geohash.length < minPrecision) {
+ return this._invalidRelayFetch(`MonitorRelayFetcher.fetchNearbyRelaysList()`, `Geohash ${geohash} is too short`);
+ }
+ if(!this?.ndk){
+ return undefined;
+ }
+ const _filter: NDKFilter = this.nip66Filter([NDKKind.RelayDiscovery], filter);
+
+ const geocodedEvents = await EventGeoCoded.fetchNearby(this.ndk, geohash, _filter, { maxPrecision, minPrecision, minResults, recurse } as FetchNearbyRelayOptions);
+ const events: Set = new Set(Array.from(geocodedEvents || new Set()).map( (event: EventGeoCoded) => (event as NDKRelayDiscovery) ));
+ const relayList: RelayListSet = this._reduceRelayEventsToRelayStrings(events);
+ return new Promise((resolve) => {
+ resolve(relayList);
+ });
+ }
+
+ protected setMonitorProfile(): Promise {
+ return new Promise( async (resolve, reject) => {
+ const profile = await this._fetchMonitorProfile()
+ if(!profile) return
+ this.profile = profile
+ resolve(this.profile)
+ })
+ }
+
+ protected setMonitorRelays(): Promise {
+ return new Promise( async (resolve, reject) => {
+ const relayList = await this._fetchMonitorRelayList()
+ if(!relayList) return
+ this.relays = relayList;
+ resolve(this.relays)
+ })
+ }
+
+ /**
+ * @description Reduces a set of `NDKEvent` objects to a list of relay strings.
+ *
+ * @param {Set} events A set of `NDKEvent` objects.
+ * @returns Promise resolves to a list of relay strings or undefined.
+ *
+ * @private
+ */
+ private _reduceRelayEventsToRelayStrings( events: Set ): RelayListSet {
+ if(typeof events === 'undefined') {
+ return new Set() as RelayListSet;
+ }
+ return new Set(Array.from(events)
+ .map( event => {
+ return event.tags
+ .filter( tag => tag[0] === 'd')
+ .map( tag => tag[1] )[0];
+ })
+ );
+ }
+
+ /**
+ * @description Handles invalid relay fetch operations by logging a warning and returning undefined.
+ *
+ * @param {string} caller The name of the calling method.
+ * @param {string} err The error message to log.
+ * @returns Always undefined, indicating an invalid operation.
+ *
+ * @private
+ */
+ private _invalidRelayFetch( caller: string, err: string ): undefined {
+ console.error(`${caller}: ${err}`);
+ return undefined;
+ }
+
+ /**
+ * @description Asynchronously fetches the Relay Monitor's profile information.
+ *
+ * @remarks
+ * This method is a private helper function intended for internal use within the class to refresh or
+ * retrieve the Relay Monitor's profile information.
+ *
+ * @private
+ * @async
+ */
+ private async _fetchMonitorProfile(): Promise {
+ const { pubkey } = this
+ const user = this.ndk?.getUser({ pubkey })
+ await user?.fetchProfile()
+ return user?.profile
+ }
+
+ /**
+ * @description Asynchronously fetches t he relay list associated with the Relay Monitor and populates the relay pool.
+ *
+ * @remarks
+ * - Returns `undefined` if `ndk` is not defined, indicating that the operation cannot be completed.
+ *
+ * @returns {Promise} A promise that resolves to an `NDKRelayList` object containing
+ * the list of relays associated with the user, or `undefined` if the operation cannot be completed.
+ *
+ * @private
+ * @async
+ */
+ private async _fetchMonitorRelayList(): Promise {
+ if(!this.ndk) return undefined;
+ const relayList: NDKRelayList | undefined = (await getRelayListForUser(this.pubkey, this.ndk)) || undefined;
+ if(relayList) {
+ relayList?.relays.forEach( (relay) => {
+ this.ndk?.pool.addRelay(new NDKRelay(relay));
+ });
+ }
+ return relayList;
+ }
+}
\ No newline at end of file
diff --git a/libraries/kit/src/index.ts b/libraries/kit/src/index.ts
new file mode 100644
index 00000000..8edf20c2
--- /dev/null
+++ b/libraries/kit/src/index.ts
@@ -0,0 +1,108 @@
+export * from './fetchers/monitor-manager';
+export * from './fetchers/monitor-fetcher';
+export * from './fetchers/monitor-relay-fetcher';
+export * from './cache';
+
+// import type NDK from '@nostr-dev-kit/ndk';
+
+// import { NDKRelayMeta } from '@nostr-dev-kit/ndk';
+
+// import { MonitorRelayFetcher } from './relay-fetcher';
+// import { KitCacheRelayMetaInterface } from './cache/default-relay-cache';
+
+// import { MonitorManager } from './monitor-manager';
+// import { MonitorCache } from './cache/default-monitor-cache';
+
+// const NWDKOptionsDefault: NWDKOptions = {
+// explicitMonitors: [],
+// monitorSelectionBias: MonitorSelectionBias.None,
+// monitorSelectionBiasValue: undefined,
+// monitorCache: undefined
+// }
+
+// const enum MonitorSelectionBias {
+// None = "none",
+// Random = "random",
+// Proximity = "proximity",
+// Country = "country"
+// }
+
+// type NWDKOptions = {
+// explicitRelays?: string[],
+// explicitMonitors?: string[],
+// monitorSelectionBias: MonitorSelectionBias,
+// monitorSelectionBiasValue?: string | number,
+// monitorCache?: KitCacheRelayMetaInterface
+// }
+
+// class NWDK {
+// private ndk: NDK;
+// private activeMonitor: MonitorRelayFetcher | undefined;
+// private bias: MonitorSelectionBias;
+
+// private _monitors: MonitorManager;
+// private _relays?: Map
+
+
+// constructor( ndk: NDK, opts: NWDKOptions ){
+// this.ndk = ndk;
+// this._monitors = new MonitorManager( this.ndk )
+// this.bias = opts.monitorSelectionBias;
+// }
+
+// async init(){
+// await this.monitors.populate();
+// this.monitors.
+// }
+
+// async autoSelectMonitor(){
+
+// }
+
+// async selectMonitor(pubkey: string){
+// this.activeMonitor = await this.monitors.cache.get(pubkey)
+// }
+
+// set KitCacheRelayMetaInterface(cache: KitCacheRelayMetaInterface){
+// if(!this.activeMonitor) return
+// this.activeMonitor.cache = cache;
+// }
+
+// set monitorCache(cache: MonitorCache){
+// this.monitors.cache = cache;
+// }
+
+// get monitors(): MonitorManager {
+// return this._monitors;
+// }
+
+// get relays(): Map