mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 13:22:56 +01:00
negentropy.
- a way to handle custom messages from Relay (NEG-whatever etc) - negentropy implementation (adapted from that other one) - nip77 nostr negentropy extension - QueryEvents method for RelayStore that returns a channel (makes negentropy syncing work more seamlessly)
This commit is contained in:
parent
b5633b97c3
commit
a094f3a9d2
12
envelopes.go
12
envelopes.go
@ -58,11 +58,6 @@ type Envelope interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventEnvelope struct {
|
|
||||||
SubscriptionID *string
|
|
||||||
Event
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ Envelope = (*EventEnvelope)(nil)
|
_ Envelope = (*EventEnvelope)(nil)
|
||||||
_ Envelope = (*ReqEnvelope)(nil)
|
_ Envelope = (*ReqEnvelope)(nil)
|
||||||
@ -74,6 +69,11 @@ var (
|
|||||||
_ Envelope = (*AuthEnvelope)(nil)
|
_ Envelope = (*AuthEnvelope)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EventEnvelope struct {
|
||||||
|
SubscriptionID *string
|
||||||
|
Event
|
||||||
|
}
|
||||||
|
|
||||||
func (_ EventEnvelope) Label() string { return "EVENT" }
|
func (_ EventEnvelope) Label() string { return "EVENT" }
|
||||||
|
|
||||||
func (v *EventEnvelope) UnmarshalJSON(data []byte) error {
|
func (v *EventEnvelope) UnmarshalJSON(data []byte) error {
|
||||||
@ -96,7 +96,7 @@ func (v EventEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
if v.SubscriptionID != nil {
|
if v.SubscriptionID != nil {
|
||||||
w.RawString(`"` + *v.SubscriptionID + `",`)
|
w.RawString(`"` + *v.SubscriptionID + `",`)
|
||||||
}
|
}
|
||||||
v.MarshalEasyJSON(&w)
|
v.Event.MarshalEasyJSON(&w)
|
||||||
w.RawString(`]`)
|
w.RawString(`]`)
|
||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
22
go.mod
22
go.mod
@ -4,22 +4,24 @@ go 1.23.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bluekeyes/go-gitdiff v0.7.1
|
github.com/bluekeyes/go-gitdiff v0.7.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
github.com/cespare/xxhash v1.1.0
|
||||||
github.com/fiatjaf/eventstore v0.8.1
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||||
|
github.com/fiatjaf/eventstore v0.9.0
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1
|
github.com/fiatjaf/generic-ristretto v0.0.1
|
||||||
github.com/gobwas/httphead v0.1.0
|
github.com/gobwas/httphead v0.1.0
|
||||||
github.com/gobwas/ws v1.3.1
|
github.com/gobwas/ws v1.4.0
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0
|
github.com/graph-gophers/dataloader/v7 v7.1.0
|
||||||
|
github.com/greatroar/blobloom v0.8.0
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tidwall/gjson v1.17.0
|
github.com/tidwall/gjson v1.17.3
|
||||||
github.com/tyler-smith/go-bip32 v1.0.0
|
github.com/tyler-smith/go-bip32 v1.0.0
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0
|
github.com/tyler-smith/go-bip39 v1.1.0
|
||||||
golang.org/x/crypto v0.14.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/text v0.15.0
|
golang.org/x/text v0.15.0
|
||||||
)
|
)
|
||||||
@ -27,10 +29,10 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
|
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
|
||||||
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
|
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/golang/glog v1.1.2 // indirect
|
github.com/golang/glog v1.1.2 // indirect
|
||||||
@ -41,6 +43,6 @@ require (
|
|||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
51
go.sum
51
go.sum
@ -2,6 +2,8 @@ github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25Yn
|
|||||||
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw=
|
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw=
|
||||||
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc=
|
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc=
|
||||||
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U=
|
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
|
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
|
||||||
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
|
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
|
||||||
@ -10,16 +12,16 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj
|
|||||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
@ -29,6 +31,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
|||||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
|
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
|
||||||
@ -39,18 +43,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fiatjaf/eventstore v0.8.1 h1:51LchQNy0Hpb0YQHwqYR5pKBpfDs/KjySlWCbbz2pkc=
|
github.com/fiatjaf/eventstore v0.9.0 h1:WsGDVAaRaVaV/J8PdrQDGfzChrL13q+lTO4C44rhu3E=
|
||||||
github.com/fiatjaf/eventstore v0.8.1/go.mod h1:bsp0Ibv0CIcVuFcoM2AEerMWmXRhF8uWXMf+dClhuow=
|
github.com/fiatjaf/eventstore v0.9.0/go.mod h1:JrAce5h0wi79+Sw4gsEq5kz0NtUxbVkOZ7lAo7ay6R8=
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM=
|
github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM=
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE=
|
github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
@ -59,8 +63,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
|||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
||||||
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@ -76,6 +80,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
||||||
|
github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4=
|
||||||
|
github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
@ -103,19 +109,24 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
@ -131,8 +142,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@ -151,8 +162,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
36
interface.go
36
interface.go
@ -7,8 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RelayStore interface {
|
type RelayStore interface {
|
||||||
Publish(ctx context.Context, event Event) error
|
Publish(context.Context, Event) error
|
||||||
QuerySync(ctx context.Context, filter Filter, opts ...SubscriptionOption) ([]*Event, error)
|
QueryEvents(context.Context, Filter) (chan *Event, error)
|
||||||
|
QuerySync(context.Context, Filter) ([]*Event, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -26,11 +27,36 @@ func (multi MultiStore) Publish(ctx context.Context, event Event) error {
|
|||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (multi MultiStore) QuerySync(ctx context.Context, filter Filter, opts ...SubscriptionOption) ([]*Event, error) {
|
func (multi MultiStore) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
|
||||||
|
multich := make(chan *Event)
|
||||||
|
|
||||||
errs := make([]error, len(multi))
|
errs := make([]error, len(multi))
|
||||||
events := make([]*Event, 0, max(filter.Limit, 10))
|
var good bool
|
||||||
for i, s := range multi {
|
for i, s := range multi {
|
||||||
res, err := s.QuerySync(ctx, filter, opts...)
|
ch, err := s.QueryEvents(ctx, filter)
|
||||||
|
errs[i] = err
|
||||||
|
if err == nil {
|
||||||
|
good = true
|
||||||
|
go func(ch chan *Event) {
|
||||||
|
for evt := range ch {
|
||||||
|
multich <- evt
|
||||||
|
}
|
||||||
|
}(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if good {
|
||||||
|
return multich, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi MultiStore) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
|
||||||
|
errs := make([]error, len(multi))
|
||||||
|
events := make([]*Event, 0, max(filter.Limit, 250))
|
||||||
|
for i, s := range multi {
|
||||||
|
res, err := s.QuerySync(ctx, filter)
|
||||||
errs[i] = err
|
errs[i] = err
|
||||||
events = append(events, res...)
|
events = append(events, res...)
|
||||||
}
|
}
|
||||||
|
181
nip77/envelopes.go
Normal file
181
nip77/envelopes.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package nip77
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
jwriter "github.com/mailru/easyjson/jwriter"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseNegMessage(message []byte) nostr.Envelope {
|
||||||
|
firstComma := bytes.Index(message, []byte{','})
|
||||||
|
if firstComma == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
label := message[0:firstComma]
|
||||||
|
|
||||||
|
var v nostr.Envelope
|
||||||
|
switch {
|
||||||
|
case bytes.Contains(label, []byte("NEG-MSG")):
|
||||||
|
v = &MessageEnvelope{}
|
||||||
|
case bytes.Contains(label, []byte("NEG-OPEN")):
|
||||||
|
v = &OpenEnvelope{}
|
||||||
|
case bytes.Contains(label, []byte("NEG-ERR")):
|
||||||
|
v = &ErrorEnvelope{}
|
||||||
|
case bytes.Contains(label, []byte("NEG-CLOSE")):
|
||||||
|
v = &CloseEnvelope{}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.UnmarshalJSON(message); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ nostr.Envelope = (*OpenEnvelope)(nil)
|
||||||
|
_ nostr.Envelope = (*MessageEnvelope)(nil)
|
||||||
|
_ nostr.Envelope = (*CloseEnvelope)(nil)
|
||||||
|
_ nostr.Envelope = (*ErrorEnvelope)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenEnvelope struct {
|
||||||
|
SubscriptionID string
|
||||||
|
Filter nostr.Filter
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ OpenEnvelope) Label() string { return "NEG-OPEN" }
|
||||||
|
func (v OpenEnvelope) String() string {
|
||||||
|
b, _ := v.MarshalJSON()
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OpenEnvelope) UnmarshalJSON(data []byte) error {
|
||||||
|
r := gjson.ParseBytes(data)
|
||||||
|
arr := r.Array()
|
||||||
|
if len(arr) != 4 {
|
||||||
|
return fmt.Errorf("failed to decode NEG-OPEN envelope")
|
||||||
|
}
|
||||||
|
|
||||||
|
v.SubscriptionID = arr[1].Str
|
||||||
|
v.Message = arr[3].Str
|
||||||
|
return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v OpenEnvelope) MarshalJSON() ([]byte, error) {
|
||||||
|
res := bytes.NewBuffer(make([]byte, 0, 17+len(v.SubscriptionID)+len(v.Message)+500))
|
||||||
|
|
||||||
|
res.WriteString(`["NEG-OPEN","`)
|
||||||
|
res.WriteString(v.SubscriptionID)
|
||||||
|
res.WriteString(`",`)
|
||||||
|
|
||||||
|
w := jwriter.Writer{}
|
||||||
|
v.Filter.MarshalEasyJSON(&w)
|
||||||
|
w.Buffer.DumpTo(res)
|
||||||
|
|
||||||
|
res.WriteString(`,"`)
|
||||||
|
res.WriteString(v.Message)
|
||||||
|
res.WriteString(`"]`)
|
||||||
|
|
||||||
|
return res.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageEnvelope struct {
|
||||||
|
SubscriptionID string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ MessageEnvelope) Label() string { return "NEG-MSG" }
|
||||||
|
func (v MessageEnvelope) String() string {
|
||||||
|
b, _ := v.MarshalJSON()
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *MessageEnvelope) UnmarshalJSON(data []byte) error {
|
||||||
|
r := gjson.ParseBytes(data)
|
||||||
|
arr := r.Array()
|
||||||
|
if len(arr) < 3 {
|
||||||
|
return fmt.Errorf("failed to decode NEG-MSG envelope")
|
||||||
|
}
|
||||||
|
v.SubscriptionID = arr[1].Str
|
||||||
|
v.Message = arr[2].Str
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v MessageEnvelope) MarshalJSON() ([]byte, error) {
|
||||||
|
res := bytes.NewBuffer(make([]byte, 0, 17+len(v.SubscriptionID)+len(v.Message)))
|
||||||
|
|
||||||
|
res.WriteString(`["NEG-MSG","`)
|
||||||
|
res.WriteString(v.SubscriptionID)
|
||||||
|
res.WriteString(`","`)
|
||||||
|
res.WriteString(v.Message)
|
||||||
|
res.WriteString(`"]`)
|
||||||
|
|
||||||
|
return res.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseEnvelope struct {
|
||||||
|
SubscriptionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ CloseEnvelope) Label() string { return "NEG-CLOSE" }
|
||||||
|
func (v CloseEnvelope) String() string {
|
||||||
|
b, _ := v.MarshalJSON()
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *CloseEnvelope) UnmarshalJSON(data []byte) error {
|
||||||
|
r := gjson.ParseBytes(data)
|
||||||
|
arr := r.Array()
|
||||||
|
if len(arr) < 2 {
|
||||||
|
return fmt.Errorf("failed to decode NEG-CLOSE envelope")
|
||||||
|
}
|
||||||
|
v.SubscriptionID = arr[1].Str
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v CloseEnvelope) MarshalJSON() ([]byte, error) {
|
||||||
|
res := bytes.NewBuffer(make([]byte, 0, 14+len(v.SubscriptionID)))
|
||||||
|
res.WriteString(`["NEG-CLOSE","`)
|
||||||
|
res.WriteString(v.SubscriptionID)
|
||||||
|
res.WriteString(`"]`)
|
||||||
|
return res.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorEnvelope struct {
|
||||||
|
SubscriptionID string
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ ErrorEnvelope) Label() string { return "NEG-ERROR" }
|
||||||
|
func (v ErrorEnvelope) String() string {
|
||||||
|
b, _ := v.MarshalJSON()
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ErrorEnvelope) UnmarshalJSON(data []byte) error {
|
||||||
|
r := gjson.ParseBytes(data)
|
||||||
|
arr := r.Array()
|
||||||
|
if len(arr) < 3 {
|
||||||
|
return fmt.Errorf("failed to decode NEG-ERROR envelope")
|
||||||
|
}
|
||||||
|
v.SubscriptionID = arr[1].Str
|
||||||
|
v.Reason = arr[2].Str
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ErrorEnvelope) MarshalJSON() ([]byte, error) {
|
||||||
|
res := bytes.NewBuffer(make([]byte, 0, 19+len(v.SubscriptionID)+len(v.Reason)))
|
||||||
|
res.WriteString(`["NEG-ERROR","`)
|
||||||
|
res.WriteString(v.SubscriptionID)
|
||||||
|
res.WriteString(`","`)
|
||||||
|
res.WriteString(v.Reason)
|
||||||
|
res.WriteString(`"]`)
|
||||||
|
return res.Bytes(), nil
|
||||||
|
}
|
61
nip77/example/example.go
Normal file
61
nip77/example/example.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/eventstore"
|
||||||
|
"github.com/fiatjaf/eventstore/slicestore"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip77"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
db := &slicestore.SliceStore{}
|
||||||
|
db.Init()
|
||||||
|
|
||||||
|
sk := nostr.GeneratePrivateKey()
|
||||||
|
local := eventstore.RelayWrapper{Store: db}
|
||||||
|
|
||||||
|
for {
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
{
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: fmt.Sprintf("same old hello %d", i),
|
||||||
|
CreatedAt: nostr.Timestamp(i),
|
||||||
|
Tags: nostr.Tags{},
|
||||||
|
}
|
||||||
|
evt.Sign(sk)
|
||||||
|
db.SaveEvent(ctx, &evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: fmt.Sprintf("custom hello %d", i),
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{},
|
||||||
|
}
|
||||||
|
evt.Sign(sk)
|
||||||
|
db.SaveEvent(ctx, &evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := nip77.NegentropySync(ctx,
|
||||||
|
local, "ws://localhost:7777", nostr.Filter{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := local.QuerySync(ctx, nostr.Filter{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("total local events:", len(data))
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
}
|
||||||
|
}
|
41
nip77/idlistpool.go
Normal file
41
nip77/idlistpool.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package nip77
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type idlistpool struct {
|
||||||
|
initialsize int
|
||||||
|
pool [][]string
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newidlistpool(initialsize int) *idlistpool {
|
||||||
|
ilp := idlistpool{
|
||||||
|
initialsize: initialsize,
|
||||||
|
pool: make([][]string, 1, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
ilp.pool[0] = make([]string, 0, initialsize)
|
||||||
|
|
||||||
|
return &ilp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ilp *idlistpool) grab() []string {
|
||||||
|
ilp.Lock()
|
||||||
|
defer ilp.Unlock()
|
||||||
|
|
||||||
|
l := len(ilp.pool)
|
||||||
|
if l > 0 {
|
||||||
|
idlist := ilp.pool[l-1]
|
||||||
|
ilp.pool = ilp.pool[0 : l-1]
|
||||||
|
return idlist
|
||||||
|
}
|
||||||
|
idlist := make([]string, 0, ilp.initialsize)
|
||||||
|
return idlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ilp *idlistpool) giveback(idlist []string) {
|
||||||
|
idlist = idlist[:0]
|
||||||
|
ilp.pool = append(ilp.pool, idlist)
|
||||||
|
}
|
126
nip77/negentropy/encoding.go
Normal file
126
nip77/negentropy/encoding.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package negentropy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n *Negentropy) DecodeTimestampIn(reader *bytes.Reader) (nostr.Timestamp, error) {
|
||||||
|
t, err := decodeVarInt(reader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := nostr.Timestamp(t)
|
||||||
|
if timestamp == 0 {
|
||||||
|
timestamp = maxTimestamp
|
||||||
|
} else {
|
||||||
|
timestamp--
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp += n.lastTimestampIn
|
||||||
|
if timestamp < n.lastTimestampIn { // Check for overflow
|
||||||
|
timestamp = maxTimestamp
|
||||||
|
}
|
||||||
|
n.lastTimestampIn = timestamp
|
||||||
|
return timestamp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) DecodeBound(reader *bytes.Reader) (Bound, error) {
|
||||||
|
timestamp, err := n.DecodeTimestampIn(reader)
|
||||||
|
if err != nil {
|
||||||
|
return Bound{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
length, err := decodeVarInt(reader)
|
||||||
|
if err != nil {
|
||||||
|
return Bound{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := make([]byte, length)
|
||||||
|
if _, err = reader.Read(id); err != nil {
|
||||||
|
return Bound{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bound{Item{timestamp, hex.EncodeToString(id)}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) encodeTimestampOut(timestamp nostr.Timestamp) []byte {
|
||||||
|
if timestamp == maxTimestamp {
|
||||||
|
n.lastTimestampOut = maxTimestamp
|
||||||
|
return encodeVarInt(0)
|
||||||
|
}
|
||||||
|
temp := timestamp
|
||||||
|
timestamp -= n.lastTimestampOut
|
||||||
|
n.lastTimestampOut = temp
|
||||||
|
return encodeVarInt(int(timestamp + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) encodeBound(bound Bound) []byte {
|
||||||
|
var output []byte
|
||||||
|
|
||||||
|
t := n.encodeTimestampOut(bound.Timestamp)
|
||||||
|
idlen := encodeVarInt(len(bound.ID) / 2)
|
||||||
|
output = append(output, t...)
|
||||||
|
output = append(output, idlen...)
|
||||||
|
id, _ := hex.DecodeString(bound.Item.ID)
|
||||||
|
|
||||||
|
output = append(output, id...)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMinimalBound(prev, curr Item) Bound {
|
||||||
|
if curr.Timestamp != prev.Timestamp {
|
||||||
|
return Bound{Item{curr.Timestamp, ""}}
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedPrefixBytes := 0
|
||||||
|
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
if curr.ID[i:i+2] != prev.ID[i:i+2] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sharedPrefixBytes++
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharedPrefixBytes + 1 to include the first differing byte, or the entire ID if identical.
|
||||||
|
return Bound{Item{curr.Timestamp, curr.ID[:(sharedPrefixBytes+1)*2]}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeVarInt(reader *bytes.Reader) (int, error) {
|
||||||
|
var res int = 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
b, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = (res << 7) | (int(b) & 127)
|
||||||
|
if (b & 128) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeVarInt(n int) []byte {
|
||||||
|
if n == 0 {
|
||||||
|
return []byte{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
var o []byte
|
||||||
|
for n != 0 {
|
||||||
|
o = append([]byte{byte(n & 0x7F)}, o...)
|
||||||
|
n >>= 7
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(o)-1; i++ {
|
||||||
|
o[i] |= 0x80
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
315
nip77/negentropy/negentropy.go
Normal file
315
nip77/negentropy/negentropy.go
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
package negentropy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
protocolVersion byte = 0x61 // version 1
|
||||||
|
maxTimestamp = nostr.Timestamp(math.MaxInt64)
|
||||||
|
)
|
||||||
|
|
||||||
|
var infiniteBound = Bound{Item: Item{Timestamp: maxTimestamp}}
|
||||||
|
|
||||||
|
type Negentropy struct {
|
||||||
|
storage Storage
|
||||||
|
sealed bool
|
||||||
|
frameSizeLimit int
|
||||||
|
isInitiator bool
|
||||||
|
lastTimestampIn nostr.Timestamp
|
||||||
|
lastTimestampOut nostr.Timestamp
|
||||||
|
|
||||||
|
Haves chan string
|
||||||
|
HaveNots chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNegentropy(storage Storage, frameSizeLimit int) *Negentropy {
|
||||||
|
return &Negentropy{
|
||||||
|
storage: storage,
|
||||||
|
frameSizeLimit: frameSizeLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) Insert(evt *nostr.Event) {
|
||||||
|
err := n.storage.Insert(evt.CreatedAt, evt.ID)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) seal() {
|
||||||
|
if !n.sealed {
|
||||||
|
n.storage.Seal()
|
||||||
|
}
|
||||||
|
n.sealed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) Initiate() []byte {
|
||||||
|
n.seal()
|
||||||
|
n.isInitiator = true
|
||||||
|
|
||||||
|
n.Haves = make(chan string, n.storage.Size()/2)
|
||||||
|
n.HaveNots = make(chan string, n.storage.Size()/2)
|
||||||
|
|
||||||
|
output := bytes.NewBuffer(make([]byte, 0, 1+n.storage.Size()*32))
|
||||||
|
output.WriteByte(protocolVersion)
|
||||||
|
n.SplitRange(0, n.storage.Size(), infiniteBound, output)
|
||||||
|
|
||||||
|
return output.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) Reconcile(msg []byte) (output []byte, err error) {
|
||||||
|
n.seal()
|
||||||
|
reader := bytes.NewReader(msg)
|
||||||
|
|
||||||
|
output, err = n.reconcileAux(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) == 1 && n.isInitiator {
|
||||||
|
close(n.Haves)
|
||||||
|
close(n.HaveNots)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
|
||||||
|
n.lastTimestampIn, n.lastTimestampOut = 0, 0 // reset for each message
|
||||||
|
|
||||||
|
fullOutput := bytes.NewBuffer(make([]byte, 0, 5000))
|
||||||
|
fullOutput.WriteByte(protocolVersion)
|
||||||
|
|
||||||
|
pv, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pv < 0x60 || pv > 0x6f {
|
||||||
|
return nil, fmt.Errorf("invalid protocol version byte")
|
||||||
|
}
|
||||||
|
if pv != protocolVersion {
|
||||||
|
if n.isInitiator {
|
||||||
|
return nil, fmt.Errorf("unsupported negentropy protocol version requested")
|
||||||
|
}
|
||||||
|
return fullOutput.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevBound Bound
|
||||||
|
prevIndex := 0
|
||||||
|
skip := false
|
||||||
|
|
||||||
|
partialOutput := bytes.NewBuffer(make([]byte, 0, 100))
|
||||||
|
for reader.Len() > 0 {
|
||||||
|
partialOutput.Reset()
|
||||||
|
|
||||||
|
doSkip := func() {
|
||||||
|
if skip {
|
||||||
|
skip = false
|
||||||
|
encodedBound := n.encodeBound(prevBound)
|
||||||
|
partialOutput.Write(encodedBound)
|
||||||
|
partialOutput.WriteByte(SkipMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currBound, err := n.DecodeBound(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
modeVal, err := decodeVarInt(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mode := Mode(modeVal)
|
||||||
|
|
||||||
|
lower := prevIndex
|
||||||
|
upper := n.storage.FindLowerBound(prevIndex, n.storage.Size(), currBound)
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case SkipMode:
|
||||||
|
skip = true
|
||||||
|
|
||||||
|
case FingerprintMode:
|
||||||
|
var theirFingerprint [FingerprintSize]byte
|
||||||
|
_, err := reader.Read(theirFingerprint[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ourFingerprint, err := n.storage.Fingerprint(lower, upper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if theirFingerprint == ourFingerprint {
|
||||||
|
skip = true
|
||||||
|
} else {
|
||||||
|
doSkip()
|
||||||
|
n.SplitRange(lower, upper, currBound, partialOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
case IdListMode:
|
||||||
|
numIds, err := decodeVarInt(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
theirElems := make(map[string]struct{})
|
||||||
|
var idb [32]byte
|
||||||
|
|
||||||
|
for i := 0; i < numIds; i++ {
|
||||||
|
_, err := reader.Read(idb[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id := hex.EncodeToString(idb[:])
|
||||||
|
theirElems[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.storage.Iterate(lower, upper, func(item Item, _ int) bool {
|
||||||
|
id := item.ID
|
||||||
|
if _, exists := theirElems[id]; !exists {
|
||||||
|
if n.isInitiator {
|
||||||
|
n.Haves <- id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete(theirElems, id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if n.isInitiator {
|
||||||
|
skip = true
|
||||||
|
for id := range theirElems {
|
||||||
|
n.HaveNots <- id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doSkip()
|
||||||
|
|
||||||
|
responseIds := make([]byte, 0, 32*n.storage.Size())
|
||||||
|
endBound := currBound
|
||||||
|
|
||||||
|
n.storage.Iterate(lower, upper, func(item Item, index int) bool {
|
||||||
|
if n.frameSizeLimit-200 < fullOutput.Len()+len(responseIds) {
|
||||||
|
endBound = Bound{item}
|
||||||
|
upper = index
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := hex.DecodeString(item.ID)
|
||||||
|
responseIds = append(responseIds, id...)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
encodedBound := n.encodeBound(endBound)
|
||||||
|
|
||||||
|
partialOutput.Write(encodedBound)
|
||||||
|
partialOutput.WriteByte(IdListMode)
|
||||||
|
partialOutput.Write(encodeVarInt(len(responseIds) / 32))
|
||||||
|
partialOutput.Write(responseIds)
|
||||||
|
|
||||||
|
partialOutput.WriteTo(fullOutput)
|
||||||
|
partialOutput.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected mode %d", mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.frameSizeLimit-200 < fullOutput.Len()+partialOutput.Len() {
|
||||||
|
// frame size limit exceeded, handle by encoding a boundary and fingerprint for the remaining range
|
||||||
|
remainingFingerprint, err := n.storage.Fingerprint(upper, n.storage.Size())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullOutput.Write(n.encodeBound(infiniteBound))
|
||||||
|
fullOutput.WriteByte(FingerprintMode)
|
||||||
|
fullOutput.Write(remainingFingerprint[:])
|
||||||
|
|
||||||
|
break // stop processing further
|
||||||
|
} else {
|
||||||
|
// append the constructed output for this iteration
|
||||||
|
partialOutput.WriteTo(fullOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevIndex = upper
|
||||||
|
prevBound = currBound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullOutput.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) SplitRange(lower, upper int, upperBound Bound, output *bytes.Buffer) {
|
||||||
|
numElems := upper - lower
|
||||||
|
const buckets = 16
|
||||||
|
|
||||||
|
if numElems < buckets*2 {
|
||||||
|
// we just send the full ids here
|
||||||
|
boundEncoded := n.encodeBound(upperBound)
|
||||||
|
output.Write(boundEncoded)
|
||||||
|
output.WriteByte(IdListMode)
|
||||||
|
output.Write(encodeVarInt(numElems))
|
||||||
|
|
||||||
|
n.storage.Iterate(lower, upper, func(item Item, _ int) bool {
|
||||||
|
id, _ := hex.DecodeString(item.ID)
|
||||||
|
output.Write(id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
itemsPerBucket := numElems / buckets
|
||||||
|
bucketsWithExtra := numElems % buckets
|
||||||
|
curr := lower
|
||||||
|
|
||||||
|
for i := 0; i < buckets; i++ {
|
||||||
|
bucketSize := itemsPerBucket
|
||||||
|
if i < bucketsWithExtra {
|
||||||
|
bucketSize++
|
||||||
|
}
|
||||||
|
ourFingerprint, err := n.storage.Fingerprint(curr, curr+bucketSize)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
curr += bucketSize
|
||||||
|
|
||||||
|
var nextBound Bound
|
||||||
|
if curr == upper {
|
||||||
|
nextBound = upperBound
|
||||||
|
} else {
|
||||||
|
var prevItem, currItem Item
|
||||||
|
|
||||||
|
n.storage.Iterate(curr-1, curr+1, func(item Item, index int) bool {
|
||||||
|
if index == curr-1 {
|
||||||
|
prevItem = item
|
||||||
|
} else {
|
||||||
|
currItem = item
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
minBound := getMinimalBound(prevItem, currItem)
|
||||||
|
nextBound = minBound
|
||||||
|
}
|
||||||
|
|
||||||
|
boundEncoded := n.encodeBound(nextBound)
|
||||||
|
output.Write(boundEncoded)
|
||||||
|
output.WriteByte(FingerprintMode)
|
||||||
|
output.Write(ourFingerprint[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Negentropy) Name() string {
|
||||||
|
p := unsafe.Pointer(n)
|
||||||
|
return fmt.Sprintf("%d", uintptr(p)&127)
|
||||||
|
}
|
111
nip77/negentropy/types.go
Normal file
111
nip77/negentropy/types.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package negentropy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FingerprintSize = 16
|
||||||
|
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SkipMode = 0
|
||||||
|
FingerprintMode = 1
|
||||||
|
IdListMode = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
Insert(nostr.Timestamp, string) error
|
||||||
|
Seal()
|
||||||
|
Size() int
|
||||||
|
Iterate(begin, end int, cb func(item Item, i int) bool) error
|
||||||
|
FindLowerBound(begin, end int, value Bound) int
|
||||||
|
GetBound(idx int) Bound
|
||||||
|
Fingerprint(begin, end int) ([FingerprintSize]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Timestamp nostr.Timestamp
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemCompare(a, b Item) int {
|
||||||
|
if a.Timestamp != b.Timestamp {
|
||||||
|
return int(a.Timestamp - b.Timestamp)
|
||||||
|
}
|
||||||
|
return strings.Compare(a.ID, b.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Item) String() string { return fmt.Sprintf("Item<%d:%s>", i.Timestamp, i.ID) }
|
||||||
|
|
||||||
|
type Bound struct{ Item }
|
||||||
|
|
||||||
|
func (b Bound) String() string {
|
||||||
|
if b.Timestamp == infiniteBound.Timestamp {
|
||||||
|
return "Bound<infinite>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Bound<%d:%s>", b.Timestamp, b.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Accumulator struct {
|
||||||
|
Buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) SetToZero() {
|
||||||
|
acc.Buf = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) Add(id string) {
|
||||||
|
b, _ := hex.DecodeString(id)
|
||||||
|
acc.AddBytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) AddAccumulator(other Accumulator) {
|
||||||
|
acc.AddBytes(other.Buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) AddBytes(other []byte) {
|
||||||
|
var currCarry, nextCarry uint32
|
||||||
|
|
||||||
|
if len(acc.Buf) < 32 {
|
||||||
|
newBuf := make([]byte, 32)
|
||||||
|
copy(newBuf, acc.Buf)
|
||||||
|
acc.Buf = newBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
offset := i * 4
|
||||||
|
orig := binary.LittleEndian.Uint32(acc.Buf[offset:])
|
||||||
|
otherV := binary.LittleEndian.Uint32(other[offset:])
|
||||||
|
|
||||||
|
next := orig + currCarry + otherV
|
||||||
|
if next < orig || next < otherV {
|
||||||
|
nextCarry = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint32(acc.Buf[offset:], next&0xFFFFFFFF)
|
||||||
|
currCarry = nextCarry
|
||||||
|
nextCarry = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) SV() []byte {
|
||||||
|
return acc.Buf[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acc *Accumulator) GetFingerprint(n int) [FingerprintSize]byte {
|
||||||
|
input := acc.SV()
|
||||||
|
input = append(input, encodeVarInt(n)...)
|
||||||
|
|
||||||
|
hash := sha256.Sum256(input)
|
||||||
|
|
||||||
|
var fingerprint [FingerprintSize]byte
|
||||||
|
copy(fingerprint[:], hash[:FingerprintSize])
|
||||||
|
return fingerprint
|
||||||
|
}
|
74
nip77/negentropy/vector.go
Normal file
74
nip77/negentropy/vector.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package negentropy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vector struct {
|
||||||
|
items []Item
|
||||||
|
sealed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVector() *Vector {
|
||||||
|
return &Vector{
|
||||||
|
items: make([]Item, 0, 30),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) Insert(createdAt nostr.Timestamp, id string) error {
|
||||||
|
if len(id)/2 != 32 {
|
||||||
|
return fmt.Errorf("bad id size for added item: expected %d, got %d", 32, len(id)/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
item := Item{createdAt, id}
|
||||||
|
v.items = append(v.items, item)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) Size() int { return len(v.items) }
|
||||||
|
|
||||||
|
func (v *Vector) Seal() {
|
||||||
|
if v.sealed {
|
||||||
|
panic("trying to seal an already sealed vector")
|
||||||
|
}
|
||||||
|
v.sealed = true
|
||||||
|
slices.SortFunc(v.items, itemCompare)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) GetBound(idx int) Bound {
|
||||||
|
if idx < len(v.items) {
|
||||||
|
return Bound{v.items[idx]}
|
||||||
|
}
|
||||||
|
return infiniteBound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) Iterate(begin, end int, cb func(Item, int) bool) error {
|
||||||
|
for i := begin; i < end; i++ {
|
||||||
|
if !cb(v.items[i], i) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) FindLowerBound(begin, end int, bound Bound) int {
|
||||||
|
idx, _ := slices.BinarySearchFunc(v.items[begin:end], bound.Item, itemCompare)
|
||||||
|
return begin + idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vector) Fingerprint(begin, end int) ([FingerprintSize]byte, error) {
|
||||||
|
var out Accumulator
|
||||||
|
out.SetToZero()
|
||||||
|
|
||||||
|
if err := v.Iterate(begin, end, func(item Item, _ int) bool {
|
||||||
|
out.Add(item.ID)
|
||||||
|
return true
|
||||||
|
}); err != nil {
|
||||||
|
return [FingerprintSize]byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.GetFingerprint(end - begin), nil
|
||||||
|
}
|
180
nip77/negentropy/whatever_test.go
Normal file
180
nip77/negentropy/whatever_test.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package negentropy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSuperSmall(t *testing.T) {
|
||||||
|
runTestWith(t,
|
||||||
|
4,
|
||||||
|
[][]int{{0, 3}}, [][]int{{2, 4}},
|
||||||
|
[][]int{{3, 4}}, [][]int{{0, 2}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoNeedToSync(t *testing.T) {
|
||||||
|
runTestWith(t,
|
||||||
|
50,
|
||||||
|
[][]int{{0, 50}}, [][]int{{0, 50}},
|
||||||
|
[][]int{}, [][]int{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmallNumbers(t *testing.T) {
|
||||||
|
runTestWith(t,
|
||||||
|
20,
|
||||||
|
[][]int{{2, 15}}, [][]int{{0, 7}, {10, 20}},
|
||||||
|
[][]int{{0, 2}, {15, 20}}, [][]int{{7, 10}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBigNumbers(t *testing.T) {
|
||||||
|
runTestWith(t,
|
||||||
|
200,
|
||||||
|
[][]int{{20, 150}}, [][]int{{0, 70}, {100, 200}},
|
||||||
|
[][]int{{0, 20}, {150, 200}}, [][]int{{70, 100}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuchBiggerNumbersAndConfusion(t *testing.T) {
|
||||||
|
runTestWith(t,
|
||||||
|
20000,
|
||||||
|
[][]int{{20, 150}, {1700, 3400}, {7000, 8100}, {13800, 13816}, {13817, 14950}, {19800, 20000}}, // n1
|
||||||
|
[][]int{{0, 2000}, {3000, 3600}, {10000, 12200}, {13799, 13801}, {14800, 19900}}, // n2
|
||||||
|
[][]int{{0, 20}, {150, 1700}, {3400, 3600}, {10000, 12200}, {13799, 13800}, {14950, 19800}}, // n1 need
|
||||||
|
[][]int{{2000, 3000}, {7000, 8100}, {13801, 13816}, {13817, 14800}, {19900, 20000}}, // n1 have
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestWith(t *testing.T,
|
||||||
|
totalEvents int,
|
||||||
|
n1Ranges [][]int, n2Ranges [][]int,
|
||||||
|
expectedN1NeedRanges [][]int, expectedN1HaveRanges [][]int,
|
||||||
|
) {
|
||||||
|
var err error
|
||||||
|
var q []byte
|
||||||
|
var n1 *Negentropy
|
||||||
|
var n2 *Negentropy
|
||||||
|
|
||||||
|
events := make([]*nostr.Event, totalEvents)
|
||||||
|
for i := range events {
|
||||||
|
evt := nostr.Event{}
|
||||||
|
evt.Content = fmt.Sprintf("event %d", i)
|
||||||
|
evt.Kind = 1
|
||||||
|
evt.CreatedAt = nostr.Timestamp(i)
|
||||||
|
evt.ID = fmt.Sprintf("%064d", i)
|
||||||
|
events[i] = &evt
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
n1 = NewNegentropy(NewVector(), 1<<16)
|
||||||
|
for _, r := range n1Ranges {
|
||||||
|
for i := r[0]; i < r[1]; i++ {
|
||||||
|
n1.Insert(events[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q = n1.Initiate()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
n2 = NewNegentropy(NewVector(), 1<<16)
|
||||||
|
for _, r := range n2Ranges {
|
||||||
|
for i := r[0]; i < r[1]; i++ {
|
||||||
|
n2.Insert(events[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err = n2.Reconcile(q)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invert := map[*Negentropy]*Negentropy{
|
||||||
|
n1: n2,
|
||||||
|
n2: n1,
|
||||||
|
}
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Done()
|
||||||
|
for n := n1; q != nil; n = invert[n] {
|
||||||
|
i++
|
||||||
|
|
||||||
|
q, err = n.Reconcile(q)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if q == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
expectedHave := make([]string, 0, 100)
|
||||||
|
for _, r := range expectedN1HaveRanges {
|
||||||
|
for i := r[0]; i < r[1]; i++ {
|
||||||
|
expectedHave = append(expectedHave, events[i].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
haves := make([]string, 0, 100)
|
||||||
|
for item := range n1.Haves {
|
||||||
|
if slices.Contains(haves, item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
haves = append(haves, item)
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, expectedHave, haves, "wrong have")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
expectedNeed := make([]string, 0, 100)
|
||||||
|
for _, r := range expectedN1NeedRanges {
|
||||||
|
for i := r[0]; i < r[1]; i++ {
|
||||||
|
expectedNeed = append(expectedNeed, events[i].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
havenots := make([]string, 0, 100)
|
||||||
|
for item := range n1.HaveNots {
|
||||||
|
if slices.Contains(havenots, item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
havenots = append(havenots, item)
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, expectedNeed, havenots, "wrong need")
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexedBytes(o []byte) string {
|
||||||
|
s := strings.Builder{}
|
||||||
|
s.Grow(2 + 1 + len(o)*5)
|
||||||
|
s.WriteString("[ ")
|
||||||
|
for _, b := range o {
|
||||||
|
x := hex.EncodeToString([]byte{b})
|
||||||
|
s.WriteString("0x")
|
||||||
|
s.WriteString(x)
|
||||||
|
s.WriteString(" ")
|
||||||
|
}
|
||||||
|
s.WriteString("]")
|
||||||
|
return s.String()
|
||||||
|
}
|
148
nip77/nip77.go
Normal file
148
nip77/nip77.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package nip77
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cespare/xxhash"
|
||||||
|
"github.com/greatroar/blobloom"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip77/negentropy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NegentropySync(ctx context.Context, store nostr.RelayStore, url string, filter nostr.Filter) error {
|
||||||
|
id := "go-nostr-tmp" // for now we can't have more than one subscription in the same connection
|
||||||
|
|
||||||
|
data, err := store.QuerySync(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query our local store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
neg := negentropy.NewNegentropy(negentropy.NewVector(), 1024*1024)
|
||||||
|
for _, evt := range data {
|
||||||
|
neg.Insert(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(chan error)
|
||||||
|
|
||||||
|
var r *nostr.Relay
|
||||||
|
r, err = nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data []byte) {
|
||||||
|
envelope := ParseNegMessage(data)
|
||||||
|
if envelope == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch env := envelope.(type) {
|
||||||
|
case *OpenEnvelope, *CloseEnvelope:
|
||||||
|
result <- fmt.Errorf("unexpected %s received from relay", env.Label())
|
||||||
|
return
|
||||||
|
case *ErrorEnvelope:
|
||||||
|
result <- fmt.Errorf("relay returned a %s: %s", env.Label(), env.Reason)
|
||||||
|
return
|
||||||
|
case *MessageEnvelope:
|
||||||
|
msg, err := hex.DecodeString(env.Message)
|
||||||
|
if err != nil {
|
||||||
|
result <- fmt.Errorf("relay sent invalid message: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextmsg, err := neg.Reconcile(msg)
|
||||||
|
if err != nil {
|
||||||
|
result <- fmt.Errorf("failed to reconcile: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nextmsg) != 0 {
|
||||||
|
msgb, _ := MessageEnvelope{id, hex.EncodeToString(nextmsg)}.MarshalJSON()
|
||||||
|
r.Write(msgb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := neg.Initiate()
|
||||||
|
open, _ := OpenEnvelope{id, filter, hex.EncodeToString(msg)}.MarshalJSON()
|
||||||
|
err = <-r.Write(open)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to relay: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
clse, _ := CloseEnvelope{id}.MarshalJSON()
|
||||||
|
r.Write(clse)
|
||||||
|
}()
|
||||||
|
|
||||||
|
type direction struct {
|
||||||
|
label string
|
||||||
|
items chan string
|
||||||
|
source nostr.RelayStore
|
||||||
|
target nostr.RelayStore
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
pool := newidlistpool(50)
|
||||||
|
for _, dir := range []direction{
|
||||||
|
{"up", neg.Haves, store, r},
|
||||||
|
{"down", neg.HaveNots, r, store},
|
||||||
|
} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(dir direction) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
seen := blobloom.NewOptimized(blobloom.Config{
|
||||||
|
Capacity: 10000,
|
||||||
|
FPRate: 0.01,
|
||||||
|
})
|
||||||
|
|
||||||
|
doSync := func(ids []string) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer pool.giveback(ids)
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
evtch, err := dir.source.QueryEvents(ctx, nostr.Filter{IDs: ids})
|
||||||
|
if err != nil {
|
||||||
|
result <- fmt.Errorf("error querying source on %s: %w", dir.label, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for evt := range evtch {
|
||||||
|
dir.target.Publish(ctx, *evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := pool.grab()
|
||||||
|
for item := range dir.items {
|
||||||
|
h := xxhash.Sum64([]byte(item))
|
||||||
|
if seen.Has(h) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.Add(h)
|
||||||
|
ids = append(ids, item)
|
||||||
|
if len(ids) == 50 {
|
||||||
|
wg.Add(1)
|
||||||
|
go doSync(ids)
|
||||||
|
ids = pool.grab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
doSync(ids)
|
||||||
|
}(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
result <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = <-result
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
59
relay.go
59
relay.go
@ -35,6 +35,7 @@ type Relay struct {
|
|||||||
|
|
||||||
challenge string // NIP-42 challenge, we only keep the last
|
challenge string // NIP-42 challenge, we only keep the last
|
||||||
noticeHandler func(string) // NIP-01 NOTICEs
|
noticeHandler func(string) // NIP-01 NOTICEs
|
||||||
|
customHandler func([]byte) // nonstandard unparseable messages
|
||||||
okCallbacks *xsync.MapOf[string, func(bool, string)]
|
okCallbacks *xsync.MapOf[string, func(bool, string)]
|
||||||
writeQueue chan writeRequest
|
writeQueue chan writeRequest
|
||||||
subscriptionChannelCloseQueue chan *Subscription
|
subscriptionChannelCloseQueue chan *Subscription
|
||||||
@ -92,6 +93,7 @@ type RelayOption interface {
|
|||||||
var (
|
var (
|
||||||
_ RelayOption = (WithNoticeHandler)(nil)
|
_ RelayOption = (WithNoticeHandler)(nil)
|
||||||
_ RelayOption = (WithSignatureChecker)(nil)
|
_ RelayOption = (WithSignatureChecker)(nil)
|
||||||
|
_ RelayOption = (WithCustomHandler)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithNoticeHandler just takes notices and is expected to do something with them.
|
// WithNoticeHandler just takes notices and is expected to do something with them.
|
||||||
@ -110,6 +112,14 @@ func (sc WithSignatureChecker) ApplyRelayOption(r *Relay) {
|
|||||||
r.signatureChecker = sc
|
r.signatureChecker = sc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCustomHandler must be a function that handles any relay message that couldn't be
|
||||||
|
// parsed as a standard envelope.
|
||||||
|
type WithCustomHandler func(data []byte)
|
||||||
|
|
||||||
|
func (ch WithCustomHandler) ApplyRelayOption(r *Relay) {
|
||||||
|
r.customHandler = ch
|
||||||
|
}
|
||||||
|
|
||||||
// String just returns the relay URL.
|
// String just returns the relay URL.
|
||||||
func (r *Relay) String() string {
|
func (r *Relay) String() string {
|
||||||
return r.URL
|
return r.URL
|
||||||
@ -185,6 +195,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
|
|||||||
}
|
}
|
||||||
case writeRequest := <-r.writeQueue:
|
case writeRequest := <-r.writeQueue:
|
||||||
// all write requests will go through this to prevent races
|
// all write requests will go through this to prevent races
|
||||||
|
debugLogf("{%s} sending %v\n", r.URL, string(writeRequest.msg))
|
||||||
if err := r.Connection.WriteMessage(r.connectionContext, writeRequest.msg); err != nil {
|
if err := r.Connection.WriteMessage(r.connectionContext, writeRequest.msg); err != nil {
|
||||||
writeRequest.answer <- err
|
writeRequest.answer <- err
|
||||||
}
|
}
|
||||||
@ -212,6 +223,9 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
|
|||||||
debugLogf("{%s} %v\n", r.URL, message)
|
debugLogf("{%s} %v\n", r.URL, message)
|
||||||
envelope := ParseMessage(message)
|
envelope := ParseMessage(message)
|
||||||
if envelope == nil {
|
if envelope == nil {
|
||||||
|
if r.customHandler != nil {
|
||||||
|
r.customHandler(message)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +354,6 @@ func (r *Relay) publish(ctx context.Context, id string, env Envelope) error {
|
|||||||
|
|
||||||
// publish event
|
// publish event
|
||||||
envb, _ := env.MarshalJSON()
|
envb, _ := env.MarshalJSON()
|
||||||
debugLogf("{%s} sending %v\n", r.URL, envb)
|
|
||||||
if err := <-r.Write(envb); err != nil {
|
if err := <-r.Write(envb); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -416,14 +429,28 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
|
|||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) QuerySync(ctx context.Context, filter Filter, opts ...SubscriptionOption) ([]*Event, error) {
|
func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
|
||||||
sub, err := r.Subscribe(ctx, Filters{filter}, opts...)
|
sub, err := r.Subscribe(ctx, Filters{filter})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer sub.Unsub()
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sub.ClosedReason:
|
||||||
|
case <-sub.EndOfStoredEvents:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-r.Context().Done():
|
||||||
|
}
|
||||||
|
sub.Unsub()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub.Events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
|
||||||
if _, ok := ctx.Deadline(); !ok {
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
// if no timeout is set, force it to 7 seconds
|
// if no timeout is set, force it to 7 seconds
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
@ -431,21 +458,17 @@ func (r *Relay) QuerySync(ctx context.Context, filter Filter, opts ...Subscripti
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
var events []*Event
|
events := make([]*Event, 0, max(filter.Limit, 250))
|
||||||
for {
|
ch, err := r.QueryEvents(ctx, filter)
|
||||||
select {
|
if err != nil {
|
||||||
case evt := <-sub.Events:
|
return nil, err
|
||||||
if evt == nil {
|
|
||||||
// channel is closed
|
|
||||||
return events, nil
|
|
||||||
}
|
|
||||||
events = append(events, evt)
|
|
||||||
case <-sub.EndOfStoredEvents:
|
|
||||||
return events, nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return events, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for evt := range ch {
|
||||||
|
events = append(events, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) Count(ctx context.Context, filters Filters, opts ...SubscriptionOption) (int64, error) {
|
func (r *Relay) Count(ctx context.Context, filters Filters, opts ...SubscriptionOption) (int64, error) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user