Browse Source

Merge pull request #180 from dskvr/develop

Update release branch
develop
Sandwich 2 years ago
committed by GitHub
parent
commit
270580581c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 4
      Dockerfile
  3. 30
      README.md
  4. 7
      package.json
  5. 5
      relays.yaml
  6. 211
      scripts/canonicals.js
  7. 0
      src/components/TaskManager.vue
  8. 49
      src/components/relays/blocks/RelaysResultTable.vue
  9. 75
      src/components/relays/blocks/RelaysSearchFilter.vue
  10. 6
      src/components/relays/nav/RelaysFindNav.vue
  11. 13
      src/components/relays/nav/RelaysNav.vue
  12. 2
      src/components/relays/pages/RelaysFind.vue
  13. 2
      src/components/relays/pages/RelaysHome.vue
  14. 4
      src/components/relays/pages/RelaysSingle.vue
  15. 14
      src/components/relays/tasks/HistoryTask.vue
  16. 194
      src/components/relays/tasks/RefreshTask.vue
  17. 158
      src/components/relays/tasks/RelayCanonicalsTask.vue
  18. 164
      src/components/relays/tasks/RelayOperatorTask.vue
  19. 81
      src/components/relays/tasks/TasksManager.vue
  20. 106
      src/components/relays/tasks/TemplateTask.vue
  21. 28
      src/components/user/AuthComponent.vue
  22. 2
      src/main.js
  23. 2
      src/shared/computed.js
  24. 13
      src/shared/relays-lib.js
  25. 19
      src/store/prefs.js
  26. 2
      src/store/profiles.js
  27. 32
      src/store/relays.js
  28. 59
      src/store/tasks.js
  29. 36
      yarn.lock

1
.gitignore

@ -5,3 +5,4 @@ public/main.js
lib/nostr-relay-inspector
dist
yarn-error.log
.env

4
Dockerfile

@ -6,9 +6,9 @@ COPY . /app/
RUN yarn && yarn build
RUN yarn global add yaml2json
#RUN yarn global add yaml2json
RUN yaml2json relays.yaml > dist/relays.json
#RUN yaml2json relays.yaml > dist/relays.json
FROM nginx:stable-alpine as nginx-nostr-relay-registry

30
README.md

@ -1,8 +1,12 @@
> this project is alpha! changes are fast and hard. branching model and tests will come with beta.
# nostr-watch
# nostr-watch 0.1.0
A client-side nostr network status built with Vue3, [nostr-js](https://github.com/jb55/nostr-js) and [nostr-relay-inspector](https://github.com/dskvr/nostr-relay-inspector). Goal is to produce a client-side app that collects detailed information about nostr relays and the network in general to assist users, developers and relay operators alike
A client-side nostr network status built with Vue3, Pinia, [nostr-tools](https://github.com/fiatjaf/nostr-tools), [nostr-js](https://github.com/jb55/nostr-js) and [nostr-relay-inspector](https://github.com/dskvr/nostr-relay-inspector).
nostr.watch aggregates data on nostr relays and the network in general to assist users, developers and relay operators.
Develop branch is deployed to https://next.nostr.watch
## Features
- [x] Real-time relay status
@ -13,25 +17,35 @@ A client-side nostr network status built with Vue3, [nostr-js](https://github.co
- [x] Relay Behavior Analysis
- [x] NIP Checks
- [x] Geo Checks (build-time)
- [ ] Optional Companion Backend for historical data and relay relief, front-end gracefully degrades if inaccessible.
- [ ] "Best Relays for a User" and "Best Relays for a Developer" dynamic aggregate, the former based largely on a balance of Latency and NIP support, the latter based largely on NIP support. Unique results for each visitor
- [x] Favorite Relays
- [x] Extension Support
- [x] Nostr signing, individualized relay testing
- [ ] Lighting Tips to Relay Operators
- [ ] Relay Statistics
- [ ] Relay Historical Data
- [ ] Discover relays at runtime (currently buildtime, ready to move to runtime with 0.1)
- [ ] Discover geo at runtime
## Todo [Road to Beta]
- [ ] Expose all features in frontend
- [ ] Clean codebase
- [ ] Tests
## Project setup
```
yarn install
```
## Pre-build
### Compiles and hot-reloads for development
Run once or whenever you want to update geo/discover relays
```
yarn prebuild
```
```
yarn serve
```
### Compiles and minifies for production
_This runs prebuild every time_
```
yarn build
```

7
package.json

@ -1,6 +1,6 @@
{
"name": "nostr-watch",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"scripts": {
"build": "vue-cli-service build",
@ -25,14 +25,15 @@
"array-timsort": "1.0.3",
"country-code-emoji": "2.3.0",
"cross-fetch": "3.1.5",
"dotenv": "16.0.3",
"fast-safe-stringify": "2.1.1",
"js-yaml": "4.1.0",
"leaflet": "1.9.3",
"node-emoji": "1.11.0",
"node-fetch": "3.3.0",
"nostr": "0.2.5",
"nostr-relay-inspector": "0.0.23",
"nostr-tools": "1.0.1",
"nostr-relay-inspector": "0.0.25",
"nostr-tools": "1.1.1",
"object-sizeof": "1.6.3",
"onion-regex": "2.0.8",
"path-segments": "0.1.1",

5
relays.yaml

@ -184,7 +184,7 @@ relays:
- wss://relay.nostr.lu.ke
- wss://relay.nostrgraph.net
- wss://private-nostr.v0l.io
- wss://relay.nvote.co:443
- "wss://relay.nvote.co:443"
- wss://relay.nostrprotocol.net
- wss://nostr.itssilvestre.com
- wss://nostr.whoop.ph
@ -198,5 +198,6 @@ relays:
- wss://relay.nostrprotocol.net
- wss://nostr.easydns.ca
- wss://nostr.bostonbtc.com
- "wss://knostr.neutrine.com:8880"
- wss://no-str.org
- wss://nostr.thesimplekid.com
- wss://knostr.neutrine.com

211
scripts/canonicals.js

@ -0,0 +1,211 @@
require('dotenv').config()
const fs = require('fs'),
yaml= require('js-yaml'),
crypto = require('crypto'),
nostrTools = require('nostr-tools'),
{ RelayPool } = require('nostr'),
{ validateEvent, verifySignature, signEvent, getEventHash, getPublicKey } = nostrTools,
uniques = new Set()
let relays = yaml.load(fs.readFileSync('./relays.yaml', 'utf8')).relays,
canonicals = new Object(),
missing = new Array(),
hashes = new Object(),
discovered = true,
totalSent = 0,
oks = 0,
notices = 0
const pool = RelayPool(relays, {reconnect: false})
pool
.on('ok', (Relay) => {
oks++
// console.log('OK', Relay.url)
})
.on('notice', (Relay, notice) => {
notices++
// console.log('NOTICE', Relay.url, notice)
})
async function run(){
// setup()
// deletions()
// await process.exit()
await discover()
// process.exit()
// console.log(`wtf`, relays.length)
// console.log(`hashes`, Object.keys(hashes).length)
await sieve()
await broadcast()
process.exit()
}
function setup(){
const event = {
"id": "a2640e8a6640c595942ccf290eae404ac58569b59af5c8c8e3334d9cf809fff6",
"pubkey": "b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6",
"created_at": 1673275222,
"kind": 1,
"tags": [],
"content": "<3 to all the relays",
"sig": "e536be52a04f95c54e5cc82caafb9b25c8d47e00182c0eac0b6b678b482710288cc7fd85c62b0f97f5ed33dfbd7e15555c9bfeac059794767e414666d807f9cf"
}
pool.send(['EVENT', event])
}
async function discover(){
console.log('relays', relays.length)
return new Promise(resolve => {
const subid = crypto.randomBytes(40).toString('hex')
pool
.on('open', Relay => {
console.log('open', Relay.url)
Relay.subscribe(subid, {limit: relays.length, kinds:[1], "#t": ['canonical'], authors:[ getPublicKey(process.env.PRIVATE_KEY) ] }, )
relays.forEach( relay => {
hashes[hash(relay)] = relay
// Relay.subscribe(`subid_${relay}`, {limit: 1, kinds:[1], authors:[ getPublicKey(process.env.PRIVATE_KEY) ] }, )
})
})
.on('event', (Relay, _subid, event) => {
if(!discovered){
// console.log('published event found', event.id)
}
if(_subid.includes(subid) && discovered) {
// console.log('event', event.content, event.id)
if(uniques.has(event.id))
return
const relayHash = event.tags.map( tag => tag[0]=='h' ? tag[1] : false )[0]
if(!relayHash)
return
// console.log('relay hash', Relay.url, relayHash)
const relay = hashes?.[relayHash]
uniques.add(event.id)
canonicals[relay] = event
}
})
setTimeout( () => {
// pool.close()
discovered = false
resolve(relays)
}, 10*1000 )
})
}
async function sieve(){
console.log('filtering relays', relays.length)
checkMissing()
console.log('missing', missing.length)
return
}
function checkMissing(){
missing = new Array()
relays.forEach( relay => {
// console.log('check missing', relay, (canonicals?.[relay] instanceof Object) )
if( !(canonicals?.[relay] instanceof Object) )
missing.push(relay)
})
}
async function broadcast(){
setInterval( ()=> {
console.log('status', '\ntotal sent:', totalSent, '\noks:', oks, '\nnotices:', notices, '\n\n')
}, 60000)
for(let i=0;i<missing.length;i++){
const relay = missing[i]
const event = {
created_at: Math.floor(Date.now()/1000),
content: `<3 ${relay}, canonical note for https://nostr.watch/relay/${relay.replace('wss://', '')}`,
kind: 1,
tags: [
['h', hash(relay)],
['t', 'canonical'],
['e', process.env.CANONICAL_NOTE, 'wss://nostr.sandwich.farm']
]
}
const signedEvent = await sign(event, relay)
if(!signedEvent)
return
// console.log("sending to pool", signedEvent)
pool.send(['EVENT', signedEvent])
totalSent++
console.log('total sent, backlog', totalSent)
await delay(10*1000)
}
console.log('finished.')
}
async function sign(event, relay){
// console.log('event to sign', event)
event.pubkey = getPublicKey(process.env.PRIVATE_KEY)
event.id = getEventHash(event)
event.sig = await signEvent(event, process.env.PRIVATE_KEY)
let ok = validateEvent(event)
let veryOk = await verifySignature(event)
// if(relay)
// console.log('sign valid', relay, ':', ok, veryOk)
// else
// console.log('sign valid', ':', ok, veryOk)
if( ok && veryOk )
return event
else
console.error('event is invalid', event)
}
async function deletions(){
const tags = [
["e", "8e68215676f0bfcc386e3cc0d9e975e7fab1aed91d781c4ec3aac5f4c2c11e24"],
["e", "00834b0779cd0a87b6eeb5d25e22e887b007a30239a1e75cb567324a687e000b"],
["e", "783f57bfbeb3c4e1cd13cc493b021cb0c353ab98c1d02f5378dfdfb0afcc77fd"]
]
const event = {
"kind": 5,
created_at: Math.floor(Date.now()/1000),
"tags": tags,
"content": "delete dev posts"
}
const signedEvent = await sign(event)
if(!signedEvent)
return
console.log("sending to pool", signedEvent)
pool.send(['EVENT', signedEvent])
return
}
async function delay(ms) {
return new Promise( resolve => setTimeout(resolve, ms) )
}
function hash(relay){
return crypto.createHash('md5').update(relay).digest('hex');
}
run()

0
src/components/TaskManager.vue

49
src/components/relays/blocks/RelaysResultTable.vue

@ -27,6 +27,10 @@
</label>
</span>
</th>
<!-- <th scope="col" class="relative py-3.5 pl-0 pr-0 sm:pr-0" v-if="isLoggedIn()">
<code class="text-xs block">Upvote</code>
</th> -->
<th scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell verified">
<!-- <span class="verified-shape-wrapper">
<span class="shape verified"></span>
@ -74,6 +78,14 @@
<a :href="`/relay/${relayClean(relay)}`">{{ relay.replace('wss://', '') }}</a>
</td>
<!-- <td class="w-16 fav text-center" v-if="isLoggedIn()">
<a
class=" hover:opacity-100 cursor-pointer opacity-20"
@click="likeRelay(relay)">
👍
</a>
</td> -->
<td class="w-12 verified text-center md:table-cell lg:table-cell xl:table-cell">
<span v-if="this.results[relay]?.identities">
<span v-tooltip:top.tooltip="identityList(relay)"> <span class="verified-shape-wrapper cursor-pointer" v-if="Object.entries(results[relay]?.identities).length"><span class="shape verified"></span></span></span>
@ -107,14 +119,15 @@
<span class="m-auto block" :class="getCheckIndicator(relay, 'write')">&nbsp;</span>
</td>
<td class="w-16 fav text-center" :key="generateKey(relay, 'check.write')">
<td class="w-16 fav text-center">
<a
class=" hover:opacity-100 cursor-pointer"
class="hover:opacity-100 cursor-pointer"
:class="store.relays.isFavorite(relay) ? 'opacity-100' : 'opacity-10'"
@click="store.relays.toggleFavorite(relay)">
</a>
</td>
</tr>
</tbody>
</table>
@ -138,10 +151,36 @@
import NostrSyncPopoverNag from '@/components/relays/partials/NostrSyncPopoverNag.vue'
import RelaysLib from '@/shared/relays-lib.js'
import UserLib from '@/shared/user-lib.js'
import {validateEvent, getEventHash, verifySignature} from 'nostr-tools'
import { setupStore } from '@/store'
const localMethods = {}
const localMethods = {
async likeRelay(relay){
const id = this.store.relays.getCanonical(relay)
const event = {
created_at: Math.floor(Date.now()/1000),
kind: 7,
content: '+',
tags: [
['e', id],
['p', this.store.user.getPublicKey]
],
pubkey: this.store.user.getPublicKey
}
event.id = getEventHash(event)
console.log('like event', event)
const signedEvent = await window.nostr.signEvent(event)
let ok = validateEvent(signedEvent)
let veryOk = await verifySignature(signedEvent)
console.log('valid event?', ok, veryOk)
},
}
export default defineComponent({
name: 'ListClearnet',
@ -207,7 +246,7 @@
},
computed: {
subsectionRelays(){
return this.sortRelays( this.store.relays.getRelays(this.subsection, this.results ) )
return this.getRelays( this.store.relays.getRelays(this.subsection, this.results ) )
},
relayGeo(){
return (relay) => this.store.relays.getGeo(relay)
@ -310,7 +349,7 @@
return (relay) => relay.replace('wss://', '')
},
},
methods: Object.assign(RelaysLib, localMethods),
methods: Object.assign(RelaysLib, UserLib, localMethods),
})
</script>

75
src/components/relays/blocks/RelaysSearchFilter.vue

@ -6,6 +6,7 @@
<MagnifyingGlassIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
ref="relay_filter"
id="relay-filter"
name="relay-filter"
placeholder="Filter Relays" type="search"
@ -13,6 +14,7 @@
/>
</div>
</div>
{{ store.prefs.getFilters }}
</template>
<script>
@ -31,7 +33,65 @@ const localMethods = {
},
clearData(){
this.store.relays.clearResults()
}
},
handleKeyUp(){
},
handleFilter(event){
this.parseFilter(event.target.value)
},
parseFilter(string){
console.log('parsing', string)
const segments = string.split(' ')
segments.forEach(segment => {
if(segment.includes('nip:'))
this.parseNip(segment)
else {
this.parseRelay(segment)
}
})
console.log(this.store.prefs.getFilters)
},
parseRelay(maybeRelay){
const fn = (relays) => {
return relays.filter( relay => relay.includes(maybeRelay))
}
this.store.prefs.addFilter( 'relay', fn )
},
parseNip(nip){
const data = nip.split(':')
if(data.length != 2)
return
const key = data[0],
value = data[1]
key
let not = false
if(data[1].startsWith('!'))
not = true
const fn = (relays) => {
return relays.filter( relay => {
const exists = this.result?.[relay]?.info?.supported_nips.includes(value)
return not ? !exists : exists
})
}
this.store.prefs.addFilter( 'relay', fn )
},
filterNip(){
},
filterCountry(){
},
filterContinent(){
}
}
export default defineComponent({
@ -44,7 +104,12 @@ export default defineComponent({
store : setupStore()
}
},
created(){
},
mounted(){
console.log('relay_filter ref', this.$refs.relay_filter)
this.$refs.relay_filter.addEventListener('change', event => this.handleFilter(event))
// this.preferences = this
},
updated(){
@ -55,13 +120,7 @@ export default defineComponent({
props: {},
data() {
return {
storage: null,
refresh: true,
preferences: {
refresh: true,
cacheExpiration: 30*60*1000
},
isActive: false,
activeFilters: {}
}
},
})

6
src/components/relays/nav/RelaysFindNav.vue

@ -14,9 +14,9 @@
</a>
</div>
</div>
<!-- <div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end">
<RelaysSearchFilter />
</div> -->
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end">
<!-- <RelaysSearchFilter /> -->
</div>
<div class="flex items-center lg:hidden">
<!-- Mobile menu button -->
<DisclosureButton class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">

13
src/components/relays/nav/RelaysNav.vue

@ -18,10 +18,9 @@
</div>
</div>
<div class="width-max lg:flex lg:ml-auto">
<RefreshTask
v-bind:resultsProp="results"
v-if="path == '/relays/find' || path.includes(`/relay/`)"/>
</div>
<TasksManager
:resultsProp="results" />
</div>
</div>
</div>
</template>
@ -35,8 +34,8 @@ import RelaysLib from '@/shared/relays-lib.js'
import { setupNavData, mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive } from '@/shared/hash-router.js'
// import RefreshTask from '@/components/relays/tasks/RefreshTask.vue'
const RefreshTask = defineAsyncComponent(() =>
import("@/components/relays/tasks/RefreshTask.vue" /* webpackChunkName: "RefreshTask" */)
const TasksManager = defineAsyncComponent(() =>
import("@/components/relays/tasks/TasksManager.vue" /* webpackChunkName: "TasksManager" */)
);
@ -44,7 +43,7 @@ export default defineComponent({
title: "nostr.watch registry & network status",
name: 'RelaysNav',
components: {
RefreshTask,
TasksManager,
},
props: {
resultsProp: {

2
src/components/relays/pages/RelaysFind.vue

@ -140,7 +140,7 @@ export default defineComponent({
this.store.relays.setGeo(geo)
this.relays = this.store.relays.getAll
this.lastUpdate = this.store.relays.lastUpdate
this.lastUpdate = this.store.tasks.getLastUpdate('relays')
this.preferences = this.store.prefs.get
},

2
src/components/relays/pages/RelaysHome.vue

@ -47,7 +47,7 @@ const localMethods = {
this.store.relays.setGeo(geo)
this.relays = this.store.relays.getAll
this.lastUpdate = this.store.relays.lastUpdate
this.lastUpdate = this.store.tasks.getLastUpdate('relays')
this.preferences = this.store.prefs.get
// this.relays.forEach(relay => {

4
src/components/relays/pages/RelaysSingle.vue

@ -121,7 +121,7 @@
<div class="py-5 col-span-3" v-if="Object.keys(result?.info).length">
<div class="py-5 col-span-3" v-if="typeof result?.info !== 'undefined'">
<div class="overflow-hidden bg-white shadow sm:rounded-lg relative">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg md:text1xl lg:text-2xl xl:text-3xl">Relay Info <code class="text-gray-300 text-xs absolute top-3 right-3">NIP-11</code></h3>
@ -697,7 +697,7 @@ export default defineComponent({
setData(){
this.relay = this.relayFromUrl
this.relays = this.store.relays.getAggregateCache('public')
this.lastUpdate = this.store.relays.getLastUpdate
this.lastUpdate = this.store.tasks.getLastUpdate('relays')
this.result = this.getCache(this.relay) || false
//
//console.log('single result', this.relayFromUrl, this.result, this.getCache(this.relay))

14
src/components/relays/tasks/HistoryTask.vue

@ -265,18 +265,18 @@ export default defineComponent({
methods: Object.assign(RelaysLib, {
invalidate: async function(){
if( !this.store.tasks.isAnyProcessing && this.isExpired() ) {
this.store.tasks.startProcessing('history')
if( !this.store.tasks.isAnyProcessing && this.isExpired('relays/history') ) {
this.store.tasks.startProcessing('relays/history')
this.store.stats.set( 'nips', this.collateSupportedNips )
this.store.tasks.addProcessed('history', 'nips')
this.store.tasks.addProcessed('relays/history', 'nips')
this.store.stats.set( 'continents', this.collateContinents )
this.store.tasks.addProcessed('history', 'continents')
this.store.tasks.addProcessed('relays/history', 'continents')
this.store.stats.set( 'countries', this.collateCountries )
this.store.tasks.addProcessed( 'history', 'countries' )
this.store.tasks.addProcessed( 'relays/history', 'countries' )
this.remoteTask = await this.historicalData()
this.store.tasks.addProcessed( 'history', 'firstSeen' )
this.store.tasks.addProcessed( 'relays/history', 'firstSeen' )
// this.store.stats.setHistory(remoteTask)
this.store.tasks.finishProcessing( 'history' )
this.store.tasks.finishProcessing( 'relays/history' )
}
},
collateSoftware(){

194
src/components/relays/tasks/RefreshTask.vue

@ -1,30 +1,31 @@
<template>
<span class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs">
<span v-if="!store.tasks.isProcessing(`relays`)">Checked {{ sinceLast }} ago</span>
<span v-if="store.tasks.isProcessing(`relays`)" class="italic lg:pr-9">
<span
v-if="!store.tasks.isActive || store.tasks.getActiveSlug === this.taskSlug"
class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs">
<span v-if="!store.tasks.isProcessing(this.taskSlug)">Checked {{ sinceLast }} ago</span>
<span v-if="store.tasks.isProcessing(this.taskSlug)" class="italic lg:pr-9">
<svg class="animate-spin mr-1 -mt-0.5 h-4 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ this.store.tasks.getProcessed('relays').length }}/{{ this.relays.length }} Relays Checked
{{ this.store.tasks.getProcessed(this.taskSlug).length }}/{{ this.relays.length }} Relays Checked
</span>
</span>
<span class="text-white text-sm mr-2 mt-1.5" v-if="!store.tasks.isProcessing(`relays`)">-</span>
<span class="text-white text-sm mr-2 mt-1.5" v-if="store.prefs.refresh && !store.tasks.isProcessing(`relays`)">
<span class="text-white text-sm mr-2 mt-1.5" v-if="!store.tasks.isProcessing(this.taskSlug)">-</span>
<span class="text-white text-sm mr-2 mt-1.5" v-if="store.prefs.refresh && !store.tasks.isProcessing(this.taskSlug)">
Next check in: {{ untilNext }}
</span>
<button
v-if="!store.tasks.isProcessing(`relays`)"
v-if="!store.tasks.isProcessing(this.taskSlug)"
class="mr-8 my-1 py-0 px-3 text-xs rounded border-b-3 border-slate-700 bg-slate-500 font-bold text-white hover:border-slate-500 hover:bg-slate-400"
:disabled='store.tasks.isProcessing(`relays`)'
:disabled='store.tasks.isProcessing(this.taskSlug)'
@click="refreshNow()">
Check{{ relay ? ` ${relay}` : "" }} Now
</button>
</template>
<style scoped>
</style>
@ -65,10 +66,11 @@ const localMethods = {
}
},
addToQueue: function(id, fn){
queueJob: function(id, fn, unique){
this.store.tasks.addJob({
id: id,
handler: fn.bind(this)
handler: fn,
unique: unique
})
},
@ -81,13 +83,14 @@ const localMethods = {
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
if(this.store.tasks.getProcessed('relays').length >= this.relays.length){
this.store.relays.updateNow()
this.store.tasks.finishProcessing('relays')
if(this.store.tasks.getProcessed(this.taskSlug).length >= this.relays.length){
this.store.tasks.updateNow(this.taskSlug)
this.store.tasks.finishProcessing(this.taskSlug)
}
if(!this.store.tasks.isProcessing)
if(!this.store.tasks.isProcessing(this.taskSlug))
this.invalidate()
}, 1000)
},
refreshNow(){
@ -102,65 +105,63 @@ const localMethods = {
if(this.windowActive)
this.store.layout.setActiveTab(this.$tabId)
},
// handleRelaysFind(){
// this.addToQueue('relays/find', () => this.invalidate())
// },
// handleRelaysSingle(relayURL){
// this.addToQueue('relays/single', () => this.invalidate(false, relayUrl))
// },
invalidate: async function(force, single){
//console.log('invalidate()', this.relays.length, force || this.isExpired )
if( (!this.isExpired && !force) )
// console.log('expired', !this.store.tasks.getLastUpdate(this.taskSlug), Date.now() - this.store.tasks.getLastUpdate(this.taskSlug) > this.store.prefs.expireAfter)
if( (!this.isExpired(this.taskSlug) && !force) )
return
// console.log('windowActive', this.windowActive)
if(!this.windowActive)
return
this.store.tasks.startProcessing('relays')
this.queueJob(this.taskSlug, async () => {
const relays = this.relays.filter( relay => !this.store.tasks.isProcessed(this.taskSlug, relay) )
console.log('unprocessed relays',
this.relays.filter( relay => !this.store.tasks.getProcessed(this.taskSlug).includes(relay)))
if(single) {
await this.check(single)
}
else {
for(let index = 0; index < relays.length; index++) {
await this.delay(this.averageLatency)
const relay = relays[index]
this.check(relay)
.then((result) => this.completeRelay(relay, result) )
// .then( async () => {
// // this.history = await History()
// })
.catch( () => this.completeRelay(relay) )
}
}
}, true)
// console.log('queue', this.store.tasks.getActive)
const relays = this.relays.filter( relay => !this.store.tasks.isProcessed('relays', relay) )
console.log('unprocessed relays',
this.relays.filter( relay => !this.store.tasks.getProcessed('relays').includes(relay)))
if(single) {
await this.check(single)
}
else {
//console.log('multiple relays', single)
// const processed = new Set()
for(let index = 0; index < relays.length; index++) {
const relay = relays[index]
//console.log('checking relay', relay)
await this.delay(this.averageLatency)
this.check(relay)
.then((result) => {
//console.log('check completed', relay)
if(this.store.tasks.isProcessed('relays', relay))
return
this.store.tasks.addProcessed('relays', result.url)
this.results[result.url] = result
this.setCache(result)
if(this.store.tasks.getProcessed('relays').length >= this.relays.length)
this.completeAll()
return this.results
})
.then( async () => {
// this.history = await History()
})
.catch( err => console.error(err) )
}
}
},
completeRelay: function(relay, result){
if(this.store.tasks.isProcessed(this.taskSlug, relay))
return
this.store.tasks.addProcessed(this.taskSlug, relay)
if(result) {
this.results[relay] = result
this.setCache(result)
}
if(this.store.tasks.getProcessed(this.taskSlug).length >= this.relays.length)
this.completeAll()
},
completeAll: function(){
//console.log('completed')
this.store.tasks.finishProcessing('relays')
this.store.relays.updateNow()
this.store.tasks.completeJob()
// this.store.tasks.finishProcessing(this.taskSlug)
this.store.tasks.updateNow(this.taskSlug)
this.store.relays.setAggregateCache('public', Object.keys(this.results).filter( result => this.results[result].aggregate === 'public' ))
this.store.relays.setAggregateCache('restricted', Object.keys(this.results).filter( result => this.results[result].aggregate === 'restricted' ))
this.store.relays.setAggregateCache('offline', Object.keys(this.results).filter( result => this.results[result].aggregate === 'offline' ))
@ -169,9 +170,7 @@ const localMethods = {
},
check: async function(relay){
console.log('this should only appear once per second')
return new Promise( (resolve, reject) => {
return new Promise( (resolve) => {
const opts = {
checkLatency: true,
getInfo: true,
@ -189,18 +188,22 @@ const localMethods = {
socket
.on('complete', (instance) => {
// console.log('completed?', instance.result)
instance.result.aggregate = this.getAggregate(instance.result)
instance.relay.close()
instance.result.log = instance.log
resolve(instance.result)
})
.on('close', () => {
//console.log(`${relay.url} has closed`)
})
.on('error', () => {
reject()
})
.run()
// .on('close', () => {
// //console.log(`${relay.url} has closed`)
// // reject()
// })
// .on('error', () => {
// // console.log(result)
// // reject()
// })
})
},
@ -220,10 +223,10 @@ const localMethods = {
return Math.floor(parseFloat(sum/total));
},
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.store.relays.lastUpdate+this.store.prefs.duration-Date.now()))
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.store.relays.getLastUpdate) || Date.now()
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
},
}
@ -237,6 +240,22 @@ export default defineComponent({
results: results
}
},
data() {
return {
relay: "",
relays: [],
refresh: {},
untilNext: null,
lastUpdate: null,
sinceLast: null,
interval: null,
windowActive: true,
averageLatency: 200,
pageOpen: 0,
taskSlug: 'relays/check'
// history: null
}
},
created(){
clearInterval(this.interval)
// document.addEventListener("visibilitychange", () => {
@ -258,7 +277,7 @@ export default defineComponent({
// document.removeEventListener("visibilitychange", this.handleVisibility, false);
},
beforeMount(){
this.lastUpdate = this.store.relays.lastUpdate
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
@ -277,7 +296,10 @@ export default defineComponent({
},
mounted(){
this.migrateLegacy()
if(this.store.tasks.isProcessing(`relays`))
// console.log('is processing', this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.taskSlug))
this.invalidate(true)
else
this.invalidate()
@ -287,7 +309,8 @@ export default defineComponent({
updated(){},
computed: Object.assign(SharedComputed, {
getDynamicTimeout: function(){
return this.averageLatency*this.relays.length
const calculated = this.averageLatency*this.relays.length
return calculated > 10000 ? calculated : 10000
},
}),
methods: Object.assign(localMethods, RelaysLib),
@ -299,21 +322,6 @@ export default defineComponent({
}
},
},
data() {
return {
relay: "",
relays: [],
refresh: {},
untilNext: null,
lastUpdate: null,
sinceLast: null,
interval: null,
windowActive: true,
averageLatency: 200,
pageOpen: 0,
// history: null
}
},
})
</script>

158
src/components/relays/tasks/RelayCanonicalsTask.vue

@ -0,0 +1,158 @@
<template>
<span
v-if="this.store.tasks.getActiveSlug === taskSlug"
class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs">
<span>Getting canonicals...</span>
</span>
</template>
<style scoped>
</style>
<script>
import crypto from 'crypto'
import { RelayPool } from 'nostr'
import { defineComponent, toRefs } from 'vue'
import { setupStore } from '@/store'
import SharedMethods from '@/shared/relays-lib.js'
import SharedComputed from '@/shared/computed.js'
import { relays } from '../../../../relays.yaml'
const localMethods = {
queueJob: function(id, fn, unique){
this.store.tasks.addJob({
id: id,
handler: fn,
unique: unique
})
},
invalidate(force){
if( (!this.isExpired(this.taskSlug) && !force) )
return
const subid = crypto.randomBytes(40).toString('hex')
this.queueJob(
this.taskSlug,
async () => {
const instance = new RelayPool(['wss://nostr.sandwich.farm', 'wss://relay.nostr.ch'])
instance
.on('open', r => {
r.subscribe(subid, {
limit: 1000,
kinds: [1],
"#t": ['canonical'],
authors:[ 'b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6' ]
})
})
.on('event', (relay, _subid, event) => {
if(_subid.includes(subid)){
console.log('canonical event', event.id)
const hash = event.tags.filter( tag => tag[0] === 'h')[0][1]
this.hashes[hash] = event.id
}
})
await this.delay(5000)
instance.unsubscribe()
instance.close()
relays.forEach( relay => {
const hash = this.hash(relay)
if( typeof this.hashes[hash] === "undefined" )
return
this.canonicals[relay] = this.hashes[hash] //event.id
})
console.log('hashes found', Object.keys(this.hashes).length)
console.log('canonicals found', Object.keys(this.canonicals).length, this.canonicals)
console.log('from store', this.store.relays.getCanonicals)
this.store.relays.setCanonicals(this.canonicals)
this.store.tasks.completeJob()
},
true
)
},
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
},
hash(relay){
return crypto.createHash('md5').update(relay).digest('hex');
}
}
export default defineComponent({
name: 'TemplateTask',
components: {},
data() {
return {
taskSlug: 'relays/canonicals',
canonicals: new Object(),
hashes: new Object()
}
},
setup(props){
const {resultsProp: results} = toRefs(props)
return {
store : setupStore(),
results: results
}
},
created(){
clearInterval(this.interval)
},
unmounted(){
clearInterval(this.interval)
},
beforeMount(){
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.relays = Array.from(new Set(relays))
},
mounted(){
console.log('task', this.taskSlug, 'is processing:', this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.taskSlug))
this.invalidate(true)
else
this.invalidate()
},
updated(){},
computed: Object.assign(SharedComputed, {
getDynamicTimeout: function(){
return this.averageLatency*this.relays.length
},
}),
methods: Object.assign(localMethods, SharedMethods),
props: {
resultsProp: {
type: Object,
default(){
return {}
}
},
},
})
</script>
<style scoped>
#refresh { font-size: 12pt; color:#666; margin-bottom:15px }
#refresh button { cursor: pointer; border-radius: 3px; border: 1px solid #a0a0a0; color:#333 }
#refresh button:hover {color:#000;}
#refresh button[disabled] {color:#999 !important; border-color:#e0e0e0}
</style>

164
src/components/relays/tasks/RelayOperatorTask.vue

@ -0,0 +1,164 @@
<template>
<span
v-if="this.store.tasks.getActiveSlug === taskSlug"
class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs">
<span>Retrieving operator profiles...</span>
</span>
</template>
<style scoped>
</style>
<script>
import { defineComponent, toRefs } from 'vue'
import crypto from 'crypto'
import { setupStore } from '@/store'
import { RelayPool } from 'nostr'
import SharedMethods from '@/shared/relays-lib.js'
import SharedComputed from '@/shared/computed.js'
import { relays } from '../../../../relays.yaml'
const localMethods = {
queueJob: function(id, fn, unique){
this.store.tasks.addJob({
id: id,
handler: fn,
unique: unique
})
},
invalidate(force){
if( !this.isExpired(this.taskSlug) && !force )
return
this.queueJob(
this.taskSlug,
() => {
const relays = this.store.relays.getAggregateCache('public')
console.log('public relays', this.store.relays.getAggregateCache('public').length)
const pool = new RelayPool(relays)
const subid = crypto.randomBytes(40).toString('hex')
const uniques = {
0: new Set(),
1: new Set(),
7: new Set(),
}
const limits = {
0: 1,
1: 20,
7: 100
}
const kinds = [0,1,7]
//remove kind 1 for non-single page tasks
pool
.on('open', relay => {
relay.subscribe(subid, { limit:10, kinds:kinds, authors:[this.result.info.pubkey] })
})
.on('event', (relay, sub_id, event) => {
console.log(event)
if(!kinds.includes(event.kind))
return
if(sub_id !== subid)
return
const u = uniques[event.kind],
l = limits[event.kind]
if( u.has(event.id) || u.size > l )
return
if( !(event instanceof Object) )
return
if( !( this.events[event.kind] instanceof Object ))
this.events[event.kind] = new Object()
this.events[event.kind][event.id] = event
u.add(event.id)
if(event.kind === 0)
this.store.profile.setProfile(JSON.parse(event.content)).catch()
console.log(`kind: ${event.kind} found`, '... total', u.size, Object.keys(this.events[event.kind]).length)
console.log( 'event!', event.content )
})
this.store.tasks.completeJob()
// .on('eose', relay => {
// relay.close()
// })
},
true
)
},
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
},
}
export default defineComponent({
name: 'TemplateTask',
components: {},
data() {
return {
taskSlug: 'relays/operatorprofiles'
}
},
setup(props){
const {resultsProp: results} = toRefs(props)
return {
store : setupStore(),
results: results
}
},
created(){
clearInterval(this.interval)
},
unmounted(){
clearInterval(this.interval)
},
beforeMount(){
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.relays = Array.from(new Set(relays))
},
mounted(){
console.log('task', this.taskSlug, 'is processing:', this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.taskSlug))
this.invalidate(true)
else
this.invalidate()
},
updated(){},
computed: Object.assign(SharedComputed, {
getDynamicTimeout: function(){
return this.averageLatency*this.relays.length
},
}),
methods: Object.assign(localMethods, SharedMethods),
props: {
resultsProp: {
type: Object,
default(){
return {}
}
},
},
})
</script>
<style scoped>
#refresh { font-size: 12pt; color:#666; margin-bottom:15px }
#refresh button { cursor: pointer; border-radius: 3px; border: 1px solid #a0a0a0; color:#333 }
#refresh button:hover {color:#000;}
#refresh button[disabled] {color:#999 !important; border-color:#e0e0e0}
</style>

81
src/components/relays/tasks/TasksManager.vue

@ -0,0 +1,81 @@
<template>
<RefreshTask
v-bind:resultsProp="results" />
<!-- <RelayCanonicalsTask
:resultsProp="results" />
<RelayOperatorTask
:resultsProp="results" /> -->
</template>
<script>
import { defineComponent, toRefs } from 'vue'
import { useRoute } from 'vue-router'
import { setupStore } from '@/store'
import SharedComputed from '@/shared/computed.js'
import RefreshTask from './RefreshTask.vue'
// import RelayCanonicalsTask from './RelayCanonicalsTask.vue'
// import RelayOperatorTask from './RelayOperatorTask.vue'
export default defineComponent({
name: "TasksManager",
components: {
RefreshTask,
// RelayCanonicalsTask,
// RelayOperatorTask
},
data(){
return {
interval: null,
currentTask: null
}
},
setup(props){
const {resultsProp: results} = toRefs(props)
return {
store : setupStore(),
results: results
}
},
beforeMount(){
//https://github.com/iendeavor/pinia-plugin-persistedstate-2/issues/136
this.store.tasks.active = new Array()
this.store.tasks.pending = new Array()
this.store.tasks.completed = new Array()
},
mounted(){
this.currentTask = this.store.tasks.currentTask
this.interval = setInterval( () => {
if(this.currentTask === this.store.tasks.currentTask)
return
this.processJob()
this.currentTask = this.store.tasks.currentTask
}, 1000)
this.processJob()
},
unmounted(){
clearInterval(this.interval)
},
props: {
resultsProp: {
type: Object,
default(){
return {}
}
},
},
methods: {
processJob(){
// console.log('trying processJob()')
if(!this.store.tasks.active?.handler)
return
this.store.tasks.active.handler()
}
},
computed: Object.assign(SharedComputed, {
path: function() { return useRoute().path },
})
});
</script>

106
src/components/relays/tasks/TemplateTask.vue

@ -0,0 +1,106 @@
<template>
<span class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs">
<span">Task Status here</span>
</span>
</template>
<style scoped>
</style>
<script>
import { defineComponent, toRefs } from 'vue'
import { setupStore } from '@/store'
import SharedMethods from '@/shared/relays-lib.js'
import SharedComputed from '@/shared/computed.js'
import { relays } from '../../../../relays.yaml'
const localMethods = {
queueJob: function(id, fn, unique){
this.store.tasks.addJob({
id: id,
handler: fn,
unique: unique
})
},
invalidate(force){
if( (!this.isExpired(this.taskSlug) && !force) )
return
},
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
},
}
export default defineComponent({
name: 'TemplateTask',
components: {},
data() {
return {
taskSlug: 'relays/*'
}
},
setup(props){
const {resultsProp: results} = toRefs(props)
return {
store : setupStore(),
results: results
}
},
created(){
clearInterval(this.interval)
},
unmounted(){
clearInterval(this.interval)
},
beforeMount(){
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.relays = Array.from(new Set(relays))
},
mounted(){
this.migrateLegacy()
console.log('is processing', this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.taskSlug))
this.invalidate(true)
else
this.invalidate()
this.setRefreshInterval()
},
updated(){},
computed: Object.assign(SharedComputed, {
getDynamicTimeout: function(){
return this.averageLatency*this.relays.length
},
}),
methods: Object.assign(localMethods, SharedMethods),
props: {
resultsProp: {
type: Object,
default(){
return {}
}
},
},
})
</script>
<style scoped>
#refresh { font-size: 12pt; color:#666; margin-bottom:15px }
#refresh button { cursor: pointer; border-radius: 3px; border: 1px solid #a0a0a0; color:#333 }
#refresh button:hover {color:#000;}
#refresh button[disabled] {color:#999 !important; border-color:#e0e0e0}
</style>

28
src/components/user/AuthComponent.vue

@ -91,24 +91,24 @@ export default defineComponent({
})
},
generateCodeChallenge: async function(codeVerifier) {
let digest = await crypto.subtle.digest("SHA-256",
new TextEncoder().encode(codeVerifier));
// generateCodeChallenge: async function(codeVerifier) {
// let digest = await crypto.subtle.digest("SHA-256",
// new TextEncoder().encode(codeVerifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
},
// return btoa(String.fromCharCode(...new Uint8Array(digest)))
// .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
// },
generateRandomString: function(length) {
let text = "";
let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// generateRandomString: function(length) {
// let text = "";
// let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
// for (let i = 0; i < length; i++) {
// text += possible.charAt(Math.floor(Math.random() * possible.length));
// }
return text;
},
// return text;
// },
}),
props: {},
})

2
src/main.js

@ -26,6 +26,8 @@ directives(app);
app.config.globalProperties.$tabId = crypto.randomBytes(40).toString('hex')
app.config.globalProperties.$filters = []
// app.config.globalProperties.$pool = new RelayPool(relays, {reconnect: false})
app.config.globalProperties.$pool = new RelayPool(['wss://relay.nostr.ch'])

2
src/shared/computed.js

@ -1,5 +1,5 @@
export default {
isExpired: function(){
return !this.store.relays.getLastUpdate || Date.now() - this.store.relays.getLastUpdate > this.store.prefs.expireAfter
return (slug) => !this.store.tasks.getLastUpdate(slug) || Date.now() - this.store.tasks.getLastUpdate(slug) > this.store.prefs.expireAfter
},
}

13
src/shared/relays-lib.js

@ -2,6 +2,19 @@ import crypto from "crypto"
import {sort} from 'array-timsort'
export default {
getRelays(relays){
relays = this.filterRelays(relays)
relays = this.sortRelays(relays)
return relays
},
filterRelays(relays){
const fns = this.store.prefs.getFilters
fns.forEach( fn => {
if(fn instanceof Function)
relays = fn(relays)
})
return relays
},
sortRelays(relays){
sort(relays, (relay1, relay2) => {
let a = this.results?.[relay1]?.latency.final || 100000,

19
src/store/prefs.js

@ -5,13 +5,16 @@ export const usePrefsStore = defineStore('prefs', {
refresh: true,
duration: 30*60*1000,
pinFavorites: true,
rowTheme: 'comfortable'
rowTheme: 'comfortable',
filters: [],
filterFn: [],
}),
getters: {
doRefresh: (state) => state.refresh,
expireAfter: (state) => state.duration,
doPinFavorites: (state) => state.pinFavorites,
getTheme: (state) => state.rowTheme,
getFilters: (state) => state.filterFn,
},
actions: {
enable(){ this.refresh = true },
@ -20,5 +23,19 @@ export const usePrefsStore = defineStore('prefs', {
updateExpiration(dur) { this.duration = dur },
togglePinFavorites(){ this.pinFavorites = !this.pinFavorites },
changeTheme(theme){ this.rowTheme = theme },
addFilter(key, fn){
if(this.filters.includes(key))
return
this.filters.push(key)
this.filterFn.push(fn)
console.log('functions:', this.filterFn)
}
},
},
{
persistedState: {
// excludePathts: ['activeFilters']
includePaths: ['refresh', 'duration', 'pinFavorites', 'rowTheme', 'filters']
// store options goes here
},
})

2
src/store/profiles.js

@ -10,7 +10,7 @@ export const useProfileStore = defineStore('profiles', {
}),
getters: {},
actions: {
set(profile){
setProfile(profile){
Object.keys(profile).forEach( key => {
if( !(profile[key] instanceof String) )
return

32
src/store/relays.js

@ -14,7 +14,8 @@ export const useRelaysStore = defineStore('relays', {
favorites: new Array(),
aggregates: {},
aggregatesAreSet: false,
cached: new Object()
cached: new Object(),
canonicals: new Object()
}),
getters: {
getAll: (state) => state.urls,
@ -25,7 +26,7 @@ export const useRelaysStore = defineStore('relays', {
return state.favorites
return state.urls.filter( (relay) => results?.[relay]?.aggregate == aggregate)
},
getByAggregate: (state) => (aggregate) => {
getByAggregate: state => aggregate => {
return state.urls
.filter( (relay) => state.results?.[relay]?.aggregate == aggregate)
},
@ -33,20 +34,23 @@ export const useRelaysStore = defineStore('relays', {
// getResults: (state) => state.results,
// getResult: (state) => (relayUrl) => state.results?.[relayUrl],
getGeo: (state) => (relayUrl) => state.geo[relayUrl],
getGeo: state => relayUrl => state.geo[relayUrl],
getLastUpdate: (state) => state.lastUpdate,
getLastUpdate: state => state.lastUpdate,
getCount: (state) => (type) => state.count[type],
getCounts: (state) => state.count,
getCount: state => type => state.count[type],
getCounts: state => state.count,
getAggregate: (state) => (which) => state.aggregates[which],
areAggregatesSet: (state) => state.aggregatesAreSet,
getAggregate: state => which => state.aggregates[which],
areAggregatesSet: state => state.aggregatesAreSet,
getFavorites: (state) => state.favorites,
isFavorite: (state) => (relayUrl) => state.favorites.includes(relayUrl),
getFavorites: state => state.favorites,
isFavorite: state => relayUrl => state.favorites.includes(relayUrl),
getAggregateCache: (state) => (aggregate) => state.cached[aggregate] instanceof Array ? state.cached[aggregate] : [],
getAggregateCache: state => aggregate => state.cached[aggregate] instanceof Array ? state.cached[aggregate] : [],
getCanonicals: state => state.canonicals,
getCanonical: state => relay => state.canonicals[relay],
},
actions: {
addRelay(relayUrl){ this.urls.push(relayUrl) },
@ -62,7 +66,7 @@ export const useRelaysStore = defineStore('relays', {
setGeo(geo){ this.geo = geo },
updateNow(){ this.lastUpdate = Date.now() },
setStat(type, value){
this.count[type] = value
@ -96,6 +100,10 @@ export const useRelaysStore = defineStore('relays', {
if( !(this.cached[aggregate] instanceof Array) )
this.cached[aggregate] = new Array()
this.cached[aggregate] = array
},
setCanonicals(c){
this.canonicals = c
}
},
})

59
src/store/tasks.js

@ -2,17 +2,21 @@ import { defineStore } from 'pinia'
export const useTaskStore = defineStore('tasks', {
state: () => ({
lastUpdate: new Object(),
//processing cache
processing: new Object(),
processed: new Object(),
currentTask: new String(),
//queue
pending: new Array(),
completed: new Array(),
active: new Object(),
//legacy
processing: new Object(),
processed: new Object(),
currentTask: new Object(),
}),
getters: {
getLastUpdate: (state) => (key) => state.lastUpdate[key],
//legacy
getProcessed: (state) => (key) => {
if( !(state.processed[key] instanceof Array) )
@ -21,38 +25,52 @@ export const useTaskStore = defineStore('tasks', {
},
isProcessing: (state) => (key) => state.processing[key],
isProcessed: (state) => (key, relay) => state.getProcessed(key).includes(relay),
isAnyProcessing: (state) => Object.keys(state.processing).filter( key => state.processing[key] ),
isAnyProcessing: (state) => Object.keys(state.processing).filter( key => state.processing[key] ).length ? true : false,
//queue/lists
getPending: (state) => state.pending,
getActive: (state) => state.active,
getActiveSlug: (state) => state.active.id,
getCompleted: (state) => state.completed,
//queue/states
isActive: (state) => Object.keys( state.active ).length > 0,
isIdle: (state) => Object.keys( state.active ).length == 0,
arePending: (state) => state.pending.length > 0,
//
// getRate: (state) => (key) => state.rate[key],
},
actions: {
updateNow(key){ this.lastUpdate[key] = Date.now() },
//queue
addJob(job){
if(job?.unique){
let exists
exists = this.active.id === job.id
if(!exists)
exists = this.pending.filter( j => j.id === job.id).length ? true : false
if(exists)
return
}
this.pending.push(job)
if( this.isIdle )
this.startNextJob()
},
startNextJob(){
if( this.isActive )
this.completed.push(this.active)
if( this.arePending ) {
this.active = this.pending[0]
this.pending.shift()
this.startProcessing(this.active)
}
else {
this.active = {}
}
},
completeJob(){
this.updateNow(this.active.id)
//there's a NaN flash, might need a delay.
this.finishProcessing(this.active.id)
this.completed.push(this.active)
this.startNextJob()
},
clearJobs(type){
this[type] = new Array()
},
@ -64,14 +82,15 @@ export const useTaskStore = defineStore('tasks', {
this.pending.splice( index, 1 )
},
//legacy
startProcessing(key) {
this.processing[key] = true
this.currentTask[key] = key
startProcessing(job) {
this.addJob(job)
this.processing[job.id] = true
this.currentTask = job.id
},
finishProcessing(key) {
this.processed[key] = new Array()
this.processing[key] = false
this.currentTask[key] = null
this.currentTask = null
},
addProcessed(key, relay){
if( !(this.processed[key] instanceof Array) )
@ -81,4 +100,16 @@ export const useTaskStore = defineStore('tasks', {
},
},
share: {
// An array of fields that the plugin will ignore.
omit: ['pending', 'completed', 'active'],
// Override global config for this store.
enable: true,
},
},
{
persistedState: {
// includePaths: ['lastUpdate', 'processed', 'processing', 'currentTask']
excludePaths: ['pending', 'completed', 'active'],
}
})

36
yarn.lock

@ -1302,7 +1302,7 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@scure/base@~1.1.0":
"@scure/base@^1.1.1", "@scure/base@~1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
@ -2503,11 +2503,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -2525,11 +2520,6 @@ batch@0.6.1:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
bech32@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355"
integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@ -3732,6 +3722,11 @@ dotenv-expand@^5.1.0:
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv@16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
dotenv@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
@ -6398,10 +6393,10 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
nostr-relay-inspector@0.0.23:
version "0.0.23"
resolved "https://registry.yarnpkg.com/nostr-relay-inspector/-/nostr-relay-inspector-0.0.23.tgz#86f3ea8a23ea1beccd18ec93db1339434679a798"
integrity sha512-G1xVvUM+PajNaZb8bAn7MGcDQL81YhKDEky1Lv2oGl0STWfrSb9KnJpSMrS8Rx/YRmGtqiNWIMfRokDCIyrKNw==
nostr-relay-inspector@0.0.25:
version "0.0.25"
resolved "https://registry.yarnpkg.com/nostr-relay-inspector/-/nostr-relay-inspector-0.0.25.tgz#8ddd0872d4529d4376b2254f32e3da2d9278963f"
integrity sha512-Xq1Y4coTC/Qd/axdGIZm6bejs6mF3A9UoHqQI8eDTGUOOvcyCIZcmowFUBkIBIVkCGCD/0DfD3vLG78Xi8oH+Q==
dependencies:
cross-fetch "3.1.5"
js-yaml "4.1.0"
@ -6410,17 +6405,16 @@ nostr-relay-inspector@0.0.23:
tape "5.6.1"
yaml-loader "0.8.0"
nostr-tools@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.0.1.tgz#acf21758fc745674ed047e7dc219b30baf489005"
integrity sha512-URBNadrVq4qSmndzd4clZqubze4y/LB8cdzzen9mNwlFh3ICDdWp7TCShTbLEZQYPPSVoOe2n13l77jzcVvH/w==
nostr-tools@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.1.tgz#2be4cd650bc0a4d20650b6cf46fee451c9f565b8"
integrity sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ==
dependencies:
"@noble/hashes" "^0.5.7"
"@noble/secp256k1" "^1.7.0"
"@scure/base" "^1.1.1"
"@scure/bip32" "^1.1.1"
"@scure/bip39" "^1.1.0"
base64-arraybuffer "^1.0.2"
bech32 "^2.0.0"
nostr@0.2.5:
version "0.2.5"

Loading…
Cancel
Save