mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
* Properly reconnect to relays, #21
* Wait for relay acknowledgements when publishing events
This commit is contained in:
parent
7514a48e63
commit
2422782015
@ -114,29 +114,30 @@ export default {
|
||||
},
|
||||
async publishPost() {
|
||||
this.publishing = true
|
||||
try {
|
||||
const event = this.ancestor
|
||||
? EventBuilder.reply(this.ancestor, this.app.myPubkey, this.content).build()
|
||||
: EventBuilder.post(this.app.myPubkey, this.content).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
|
||||
const event = this.ancestor
|
||||
? EventBuilder.reply(this.ancestor, this.app.myPubkey, this.content).build()
|
||||
: EventBuilder.post(this.app.myPubkey, this.content).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
|
||||
const numRelays = await this.nostr.publish(event)
|
||||
if (numRelays) {
|
||||
this.reset()
|
||||
this.$emit('publish', event)
|
||||
|
||||
// TODO i18n
|
||||
const postType = this.ancestor ? 'Reply' : 'Post'
|
||||
this.$q.notify({
|
||||
message: `${postType} published`,
|
||||
message: `${postType} published to ${numRelays} relays`,
|
||||
color: 'positive',
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to publish post', e)
|
||||
} else {
|
||||
this.$q.notify({
|
||||
message: `Failed to publish post`,
|
||||
color: 'negative'
|
||||
})
|
||||
}
|
||||
|
||||
this.publishing = false
|
||||
},
|
||||
},
|
||||
|
@ -94,24 +94,23 @@ export default {
|
||||
},
|
||||
async publishMessage() {
|
||||
this.publishing = true
|
||||
try {
|
||||
const ciphertext = await this.app.encryptMessage(this.recipient, this.content)
|
||||
if (!ciphertext) return
|
||||
const event = EventBuilder.message(this.app.myPubkey, this.recipient, ciphertext).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
|
||||
const ciphertext = await this.app.encryptMessage(this.recipient, this.content)
|
||||
if (!ciphertext) return
|
||||
const event = EventBuilder.message(this.app.myPubkey, this.recipient, ciphertext).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
|
||||
if (await this.nostr.publish(event)) {
|
||||
this.reset()
|
||||
this.$nextTick(this.focus.bind(this))
|
||||
|
||||
this.$emit('publish', event)
|
||||
} catch (e) {
|
||||
console.error('Failed to send message', e)
|
||||
} else {
|
||||
this.$q.notify({
|
||||
message: `Failed to send message`,
|
||||
color: 'negative'
|
||||
})
|
||||
}
|
||||
|
||||
this.publishing = false
|
||||
},
|
||||
},
|
||||
|
@ -76,13 +76,23 @@ export default {
|
||||
async publishLike() {
|
||||
const event = EventBuilder.reaction(this.note, this.app.myPubkey).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
if (!await this.nostr.publish(event)) {
|
||||
this.$q.notify({
|
||||
message: 'Failed to publish reaction',
|
||||
color: 'negative',
|
||||
})
|
||||
}
|
||||
},
|
||||
async deleteLike() {
|
||||
const ids = this.ourReactions.map(r => r.id)
|
||||
const event = EventBuilder.delete(this.app.myPubkey, ids).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
if (!await this.nostr.publish(event)) {
|
||||
this.$q.notify({
|
||||
message: 'Failed to delete reaction',
|
||||
color: 'negative',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -78,7 +78,12 @@ export default {
|
||||
}
|
||||
const event = EventBuilder.metadata(this.pubkey, metadata).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
if (!await this.nostr.publish(event)) {
|
||||
this.$q.notify({
|
||||
message: 'Failed to update profile',
|
||||
color: 'negative'
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -41,12 +41,17 @@ export default {
|
||||
|
||||
const event = EventBuilder.metadata(account.pubkey, {name: this.username}).build()
|
||||
await useAppStore().signEvent(event)
|
||||
useNostrStore().publish(event)
|
||||
|
||||
this.$emit('complete', {
|
||||
pubkey: account.pubkey,
|
||||
name: this.username
|
||||
})
|
||||
if (await useNostrStore().publish(event)) {
|
||||
this.$emit('complete', {
|
||||
pubkey: account.pubkey,
|
||||
name: this.username
|
||||
})
|
||||
} else {
|
||||
this.$q.notify({
|
||||
message: 'Failed to create profile',
|
||||
color: 'negative',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -41,7 +41,12 @@ export default {
|
||||
async updateContacts(contacts) {
|
||||
const event = EventBuilder.contacts(this.app.myPubkey, contacts.map(c => c.pubkey)).build()
|
||||
if (!await this.app.signEvent(event)) return
|
||||
this.nostr.publish(event)
|
||||
if (!await this.nostr.publish(event)) {
|
||||
this.$q.notify({
|
||||
message: 'Failed to update followers',
|
||||
color: 'negative',
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleFollow() {
|
||||
return this.isFollowing
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {Observable} from 'src/nostr/Observable'
|
||||
|
||||
export default class FetchQueue extends Observable {
|
||||
constructor(client, subId, fnGetId, fnCreateFilter, opts = {}) {
|
||||
|
@ -10,7 +10,7 @@ import {useSettingsStore} from 'stores/Settings'
|
||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useMessageStore} from 'src/nostr/store/MessageStore'
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {Observable} from 'src/nostr/Observable'
|
||||
import {CloseAfter} from 'src/nostr/Relay'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
|
||||
@ -137,10 +137,13 @@ export const useNostrStore = defineStore('nostr', {
|
||||
return !!this.seenBy[id]
|
||||
},
|
||||
|
||||
publish(event) {
|
||||
// FIXME represent 'local' somehow
|
||||
this.addEvent(event, {url: '<local>'})
|
||||
return this.client.publish(event)
|
||||
async publish(event) {
|
||||
const result = await this.client.publish(event)
|
||||
if (result) {
|
||||
// FIXME represent 'local' somehow
|
||||
this.addEvent(event, {url: '<local>'})
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
subscribeForUser(pubkey) {
|
||||
|
43
src/nostr/Observable.js
Normal file
43
src/nostr/Observable.js
Normal file
@ -0,0 +1,43 @@
|
||||
export class Observable {
|
||||
constructor() {
|
||||
this.listeners = {}
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
this.addListener(event, {callback, once: false})
|
||||
}
|
||||
|
||||
once(event, callback) {
|
||||
this.addListener(event, {callback, once: true})
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
const listeners = this.listeners[event]
|
||||
if (!listeners) return
|
||||
const idx = listeners.findIndex(listener => listener.callback === callback)
|
||||
if (idx >= 0) listeners.splice(idx, 1)
|
||||
}
|
||||
|
||||
addListener(event, listener) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [listener]
|
||||
} else {
|
||||
this.listeners[event].push(listener)
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
const listeners = this.listeners[event]
|
||||
if (!listeners) return
|
||||
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener.callback.apply(null, args)
|
||||
} catch (e) {
|
||||
console.error(`Exception thrown from '${event}' listener: ${e.message || e}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners[event] = listeners.filter(listener => !listener.once)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {Observable} from 'src/nostr/Observable'
|
||||
import Event from 'src/nostr/model/Event'
|
||||
|
||||
export class Subscription extends Observable {
|
||||
@ -75,7 +75,26 @@ export class Relay extends Observable {
|
||||
}
|
||||
|
||||
publish(event) {
|
||||
this.socket.send(['EVENT', event])
|
||||
return new Promise(resolve => {
|
||||
if (!this.socket.send(['EVENT', event])) {
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
let timeout
|
||||
const callback = (eventId, wasSaved) => {
|
||||
if (eventId === event.id && wasSaved) {
|
||||
clearTimeout(timeout)
|
||||
this.off('ok', callback)
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
this.off('ok', callback)
|
||||
resolve(false)
|
||||
}, 4000) // TODO make this a parameter
|
||||
|
||||
this.on('ok', callback)
|
||||
})
|
||||
}
|
||||
|
||||
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||
@ -176,11 +195,15 @@ class ReconnectingWebSocket extends Observable {
|
||||
this.disconnected = false
|
||||
this.reconnectAfter = this.opts.reconnectAfter
|
||||
this.reconnectTimer = null
|
||||
|
||||
window.addEventListener('online', this.connect.bind(this))
|
||||
window.addEventListener('focus', this.connect.bind(this))
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.socket) return
|
||||
if (this.isConnected()) return
|
||||
this.disconnected = false
|
||||
this.reconnectTimer = null
|
||||
|
||||
const ws = new WebSocket(this.url)
|
||||
ws.onopen = this.onOpen.bind(this)
|
||||
@ -192,36 +215,47 @@ class ReconnectingWebSocket extends Observable {
|
||||
|
||||
disconnect() {
|
||||
this.disconnected = true
|
||||
if (this.socket) this.socket.close()
|
||||
this.socket = null
|
||||
this.close()
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.disconnected || this.reconnectTimer) return
|
||||
|
||||
console.log(`[RELAY] Scheduling reconnect to ${this.url} in ${this.reconnectAfter}ms`)
|
||||
this.reconnectTimer = setTimeout(
|
||||
() => {
|
||||
this.connect()
|
||||
console.log(`[RELAY] Reconnecting to ${this.url} now`)
|
||||
this.reconnectTimer = null
|
||||
this.connect()
|
||||
},
|
||||
this.reconnectAfter
|
||||
)
|
||||
|
||||
this.reconnectAfter *= 2
|
||||
this.reconnectAfter = Math.min(this.reconnectAfter *= 2, 1000 * 60 * 5)
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.socket && this.socket.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.socket) this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
|
||||
send(message) {
|
||||
// TODO Wait for connected?
|
||||
if (!this.isConnected()) {
|
||||
console.warn(`Not connected to ${this.url} (currently ${this.socket?.readyState})`)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
this.socket.send(JSON.stringify(message))
|
||||
try {
|
||||
this.socket.send(JSON.stringify(message))
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
@ -230,15 +264,19 @@ class ReconnectingWebSocket extends Observable {
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.close()
|
||||
this.emit('close', this)
|
||||
if (this.opts.reconnect) this.reconnect()
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
console.log(`Socket error from relay ${this.url}`, error)
|
||||
|
||||
this.emit('error', error, this)
|
||||
if (this.opts.reconnect) this.reconnect()
|
||||
|
||||
if (!this.isConnected()) {
|
||||
this.close()
|
||||
if (this.opts.reconnect) this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(message) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {CloseAfter, Relay} from 'src/nostr/Relay'
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {Observable} from 'src/nostr/Observable'
|
||||
|
||||
class MultiSubscription extends Observable {
|
||||
constructor(subId, subs) {
|
||||
@ -95,10 +95,17 @@ export default class ReplayPool extends Observable {
|
||||
delete this.relays[url]
|
||||
}
|
||||
|
||||
publish(event) {
|
||||
async publish(event) {
|
||||
const promises = []
|
||||
for (const relay of this.connectedRelays()) {
|
||||
relay.publish(event)
|
||||
promises.push(relay.publish(event))
|
||||
}
|
||||
return Promise.all(promises)
|
||||
.then(results => results.filter(res => res).length)
|
||||
.catch(e => {
|
||||
console.error('Error while publishing', e)
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||
|
@ -1,26 +0,0 @@
|
||||
export class Observable {
|
||||
constructor() {
|
||||
this.listeners = {}
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [callback]
|
||||
} else {
|
||||
this.listeners[event].push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
const listeners = this.listeners[event]
|
||||
if (!listeners) return
|
||||
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener.apply(null, args)
|
||||
} catch (e) {
|
||||
console.error(`Exception thrown from '${event}' listener: ${e.message || e}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user