mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 21:32:56 +01:00
implement envelope, event and filter parsing with simdjson-go.
This commit is contained in:
parent
6d8cd55784
commit
56e9a5a709
253
envelopes.go
253
envelopes.go
@ -3,14 +3,78 @@ package nostr
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
"github.com/minio/simdjson-go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var (
|
||||
labelEvent = []byte("EVENT")
|
||||
labelReq = []byte("REQ")
|
||||
labelCount = []byte("COUNT")
|
||||
labelNotice = []byte("NOTICE")
|
||||
labelEose = []byte("EOSE")
|
||||
labelOk = []byte("OK")
|
||||
labelAuth = []byte("AUTH")
|
||||
labelClosed = []byte("CLOSED")
|
||||
labelClose = []byte("CLOSE")
|
||||
|
||||
UnknownLabel = errors.New("unknown envelope label")
|
||||
)
|
||||
|
||||
func ParseMessageSIMD(message []byte, reuse *simdjson.ParsedJson) (Envelope, error) {
|
||||
parsed, err := simdjson.Parse(message, reuse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("simdjson parse failed: %w", err)
|
||||
}
|
||||
|
||||
iter := parsed.Iter()
|
||||
iter.AdvanceInto()
|
||||
if t := iter.Advance(); t != simdjson.TypeArray {
|
||||
return nil, fmt.Errorf("top-level must be an array")
|
||||
}
|
||||
arr, _ := iter.Array(nil)
|
||||
iter = arr.Iter()
|
||||
iter.Advance()
|
||||
label, _ := iter.StringBytes()
|
||||
|
||||
var v Envelope
|
||||
|
||||
switch {
|
||||
case bytes.Equal(label, labelEvent):
|
||||
v = &EventEnvelope{}
|
||||
case bytes.Equal(label, labelReq):
|
||||
v = &ReqEnvelope{}
|
||||
case bytes.Equal(label, labelCount):
|
||||
v = &CountEnvelope{}
|
||||
case bytes.Equal(label, labelNotice):
|
||||
x := NoticeEnvelope("")
|
||||
v = &x
|
||||
case bytes.Equal(label, labelEose):
|
||||
x := EOSEEnvelope("")
|
||||
v = &x
|
||||
case bytes.Equal(label, labelOk):
|
||||
v = &OKEnvelope{}
|
||||
case bytes.Equal(label, labelAuth):
|
||||
v = &AuthEnvelope{}
|
||||
case bytes.Equal(label, labelClosed):
|
||||
v = &ClosedEnvelope{}
|
||||
case bytes.Equal(label, labelClose):
|
||||
x := CloseEnvelope("")
|
||||
v = &x
|
||||
default:
|
||||
return nil, UnknownLabel
|
||||
}
|
||||
|
||||
err = v.UnmarshalSIMD(iter)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func ParseMessage(message []byte) Envelope {
|
||||
firstComma := bytes.Index(message, []byte{','})
|
||||
if firstComma == -1 {
|
||||
@ -20,25 +84,25 @@ func ParseMessage(message []byte) Envelope {
|
||||
|
||||
var v Envelope
|
||||
switch {
|
||||
case bytes.Contains(label, []byte("EVENT")):
|
||||
case bytes.Contains(label, labelEvent):
|
||||
v = &EventEnvelope{}
|
||||
case bytes.Contains(label, []byte("REQ")):
|
||||
case bytes.Contains(label, labelReq):
|
||||
v = &ReqEnvelope{}
|
||||
case bytes.Contains(label, []byte("COUNT")):
|
||||
case bytes.Contains(label, labelCount):
|
||||
v = &CountEnvelope{}
|
||||
case bytes.Contains(label, []byte("NOTICE")):
|
||||
case bytes.Contains(label, labelNotice):
|
||||
x := NoticeEnvelope("")
|
||||
v = &x
|
||||
case bytes.Contains(label, []byte("EOSE")):
|
||||
case bytes.Contains(label, labelEose):
|
||||
x := EOSEEnvelope("")
|
||||
v = &x
|
||||
case bytes.Contains(label, []byte("OK")):
|
||||
case bytes.Contains(label, labelOk):
|
||||
v = &OKEnvelope{}
|
||||
case bytes.Contains(label, []byte("AUTH")):
|
||||
case bytes.Contains(label, labelAuth):
|
||||
v = &AuthEnvelope{}
|
||||
case bytes.Contains(label, []byte("CLOSED")):
|
||||
case bytes.Contains(label, labelClosed):
|
||||
v = &ClosedEnvelope{}
|
||||
case bytes.Contains(label, []byte("CLOSE")):
|
||||
case bytes.Contains(label, labelClose):
|
||||
x := CloseEnvelope("")
|
||||
v = &x
|
||||
default:
|
||||
@ -53,6 +117,7 @@ func ParseMessage(message []byte) Envelope {
|
||||
|
||||
type Envelope interface {
|
||||
Label() string
|
||||
UnmarshalSIMD(simdjson.Iter) error
|
||||
UnmarshalJSON([]byte) error
|
||||
MarshalJSON() ([]byte, error)
|
||||
String() string
|
||||
@ -90,6 +155,25 @@ func (v *EventEnvelope) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *EventEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
// we may or may not have a subscription ID, so peek
|
||||
if iter.PeekNext() == simdjson.TypeString {
|
||||
iter.Advance()
|
||||
// we have a subscription ID
|
||||
subID, err := iter.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.SubscriptionID = &subID
|
||||
}
|
||||
|
||||
// now get the event
|
||||
if typ := iter.Advance(); typ == simdjson.TypeNone {
|
||||
return fmt.Errorf("missing event")
|
||||
}
|
||||
return v.Event.UnmarshalSIMD(&iter)
|
||||
}
|
||||
|
||||
func (v EventEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["EVENT",`)
|
||||
@ -129,6 +213,44 @@ func (v *ReqEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ReqEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
var err error
|
||||
|
||||
// we must have a subscription id
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.SubscriptionID, err = iter.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unexpected %s for REQ subscription id", typ)
|
||||
}
|
||||
|
||||
// now get the filters
|
||||
v.Filters = make(Filters, 0, 1)
|
||||
tempIter := &simdjson.Iter{} // make a new iterator here because there may come multiple filters
|
||||
for {
|
||||
if typ, err := iter.AdvanceIter(tempIter); err != nil {
|
||||
return err
|
||||
} else if typ == simdjson.TypeNone {
|
||||
break
|
||||
} else {
|
||||
}
|
||||
|
||||
var filter Filter
|
||||
if err := filter.UnmarshalSIMD(tempIter); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Filters = append(v.Filters, filter)
|
||||
}
|
||||
|
||||
if len(v.Filters) == 0 {
|
||||
return fmt.Errorf("need at least one filter")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v ReqEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["REQ","`)
|
||||
@ -193,6 +315,53 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *CountEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
var err error
|
||||
|
||||
// this has two cases:
|
||||
// in the first case (request from client) this is like REQ except with always one filter
|
||||
// in the other (response from relay) we have a json object response
|
||||
// but both cases start with a subscription id
|
||||
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.SubscriptionID, err = iter.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unexpected %s for COUNT subscription id", typ)
|
||||
}
|
||||
|
||||
// now get either a single filter or stuff from the json object
|
||||
if typ := iter.Advance(); typ == simdjson.TypeNone {
|
||||
return fmt.Errorf("missing json object")
|
||||
}
|
||||
|
||||
if el, err := iter.FindElement(nil, "count"); err == nil {
|
||||
c, _ := el.Iter.Uint()
|
||||
count := int64(c)
|
||||
v.Count = &count
|
||||
if el, err = iter.FindElement(nil, "hll"); err == nil {
|
||||
if hllHex, err := el.Iter.StringBytes(); err != nil || len(hllHex) != 512 {
|
||||
return fmt.Errorf("hll is malformed")
|
||||
} else {
|
||||
v.HyperLogLog = make([]byte, 256)
|
||||
if _, err := hex.Decode(v.HyperLogLog, hllHex); err != nil {
|
||||
return fmt.Errorf("hll is invalid hex")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var filter Filter
|
||||
if err := filter.UnmarshalSIMD(&iter); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Filters = Filters{filter}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v CountEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["COUNT","`)
|
||||
@ -237,6 +406,14 @@ func (v *NoticeEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *NoticeEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
msg, _ := iter.String()
|
||||
*v = NoticeEnvelope(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v NoticeEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["NOTICE",`)
|
||||
@ -263,6 +440,14 @@ func (v *EOSEEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *EOSEEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
msg, _ := iter.String()
|
||||
*v = EOSEEnvelope(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v EOSEEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["EOSE",`)
|
||||
@ -291,6 +476,14 @@ func (v *CloseEnvelope) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *CloseEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
msg, _ := iter.String()
|
||||
*v = CloseEnvelope(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v CloseEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["CLOSE",`)
|
||||
@ -322,6 +515,16 @@ func (v *ClosedEnvelope) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ClosedEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.SubscriptionID, _ = iter.String()
|
||||
}
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.Reason, _ = iter.String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v ClosedEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["CLOSED",`)
|
||||
@ -357,6 +560,23 @@ func (v *OKEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *OKEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.EventID, _ = iter.String()
|
||||
} else {
|
||||
return fmt.Errorf("unexpected %s for OK id", typ)
|
||||
}
|
||||
if typ := iter.Advance(); typ == simdjson.TypeBool {
|
||||
v.OK, _ = iter.Bool()
|
||||
} else {
|
||||
return fmt.Errorf("unexpected %s for OK status", typ)
|
||||
}
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
v.Reason, _ = iter.String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v OKEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["OK","`)
|
||||
@ -398,6 +618,21 @@ func (v *AuthEnvelope) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *AuthEnvelope) UnmarshalSIMD(iter simdjson.Iter) error {
|
||||
if typ := iter.Advance(); typ == simdjson.TypeString {
|
||||
// we have a challenge
|
||||
subID, err := iter.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Challenge = &subID
|
||||
return nil
|
||||
} else {
|
||||
// we have an event
|
||||
return v.Event.UnmarshalSIMD(&iter)
|
||||
}
|
||||
}
|
||||
|
||||
func (v AuthEnvelope) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{NoEscapeHTML: true}
|
||||
w.RawString(`["AUTH",`)
|
||||
|
@ -3,7 +3,9 @@ package nostr
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/minio/simdjson-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEventEnvelopeEncodingAndDecoding(t *testing.T) {
|
||||
@ -174,4 +176,155 @@ func TestParseMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMessageSIMD(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Message []byte
|
||||
ExpectedEnvelope Envelope
|
||||
ExpectedErrorSubstring string
|
||||
}{
|
||||
{
|
||||
Name: "nil",
|
||||
Message: nil,
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "parse failed",
|
||||
},
|
||||
{
|
||||
Name: "empty string",
|
||||
Message: []byte(""),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "parse failed",
|
||||
},
|
||||
{
|
||||
Name: "invalid input",
|
||||
Message: []byte("invalid input"),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "parse failed",
|
||||
},
|
||||
{
|
||||
Name: "invalid JSON",
|
||||
Message: []byte("{not valid json}"),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "parse failed",
|
||||
},
|
||||
{
|
||||
Name: "invalid REQ",
|
||||
Message: []byte(`["REQ","zzz", {"authors": [23]}]`),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "not string, but int",
|
||||
},
|
||||
{
|
||||
Name: "same invalid REQ from before, but now valid",
|
||||
Message: []byte(`["REQ","zzz", {"kinds": [23]}]`),
|
||||
ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "zzz", Filters: Filters{{Kinds: []int{23}}}},
|
||||
},
|
||||
{
|
||||
Name: "different invalid REQ",
|
||||
Message: []byte(`["REQ","zzz", {"authors": "string"}]`),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "next item is not",
|
||||
},
|
||||
{
|
||||
Name: "yet another",
|
||||
Message: []byte(`["REQ","zzz", {"unknownfield": "_"}]`),
|
||||
ExpectedEnvelope: nil,
|
||||
ExpectedErrorSubstring: "unexpected filter field 'unknownfield'",
|
||||
},
|
||||
{
|
||||
Name: "EVENT envelope with subscription id",
|
||||
Message: []byte(
|
||||
`["EVENT","_",{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}]`,
|
||||
),
|
||||
ExpectedEnvelope: &EventEnvelope{SubscriptionID: ptr("_"), Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}},
|
||||
},
|
||||
{
|
||||
Name: "EVENT envelope without subscription id",
|
||||
Message: []byte(
|
||||
`["EVENT",{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}]`,
|
||||
),
|
||||
ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}},
|
||||
},
|
||||
{
|
||||
Name: "AUTH envelope with challenge",
|
||||
Message: []byte(`["AUTH","challenge-string"]`),
|
||||
ExpectedEnvelope: &AuthEnvelope{Challenge: ptr("challenge-string")},
|
||||
},
|
||||
{
|
||||
Name: "AUTH envelope with event",
|
||||
Message: []byte(
|
||||
`["AUTH", {"kind":22242,"id":"9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1740505646,"tags":[["relay","ws://localhost:7777","2"],["challenge","3027526784722639360"]],"content":"","sig":"eceb827c4bba1de0ab8ee43f3e98df71194f5bdde0af27b5cda38e5c4338b5f63d31961acb5e3c119fd00ecef8b469867d060b697dbaa6ecee1906b483bc307d"}]`,
|
||||
),
|
||||
ExpectedEnvelope: &AuthEnvelope{Event: Event{Kind: 22242, ID: "9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a", PubKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", CreatedAt: 1740505646, Tags: Tags{Tag{"relay", "ws://localhost:7777", "2"}, Tag{"challenge", "3027526784722639360"}}, Content: "", Sig: "eceb827c4bba1de0ab8ee43f3e98df71194f5bdde0af27b5cda38e5c4338b5f63d31961acb5e3c119fd00ecef8b469867d060b697dbaa6ecee1906b483bc307d"}},
|
||||
},
|
||||
{
|
||||
Name: "NOTICE envelope",
|
||||
Message: []byte(`["NOTICE","test notice message"]`),
|
||||
ExpectedEnvelope: ptr(NoticeEnvelope("test notice message")),
|
||||
},
|
||||
{
|
||||
Name: "EOSE envelope",
|
||||
Message: []byte(`["EOSE","subscription123"]`),
|
||||
ExpectedEnvelope: ptr(EOSEEnvelope("subscription123")),
|
||||
},
|
||||
{
|
||||
Name: "CLOSE envelope",
|
||||
Message: []byte(`["CLOSE","subscription123"]`),
|
||||
ExpectedEnvelope: ptr(CloseEnvelope("subscription123")),
|
||||
},
|
||||
{
|
||||
Name: "CLOSED envelope",
|
||||
Message: []byte(`["CLOSED","subscription123","reason: test closed"]`),
|
||||
ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: "subscription123", Reason: "reason: test closed"},
|
||||
},
|
||||
{
|
||||
Name: "OK envelope",
|
||||
Message: []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`),
|
||||
ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: true, Reason: ""},
|
||||
},
|
||||
{
|
||||
Name: "COUNT envelope with just count",
|
||||
Message: []byte(`["COUNT","sub1",{"count":42}]`),
|
||||
ExpectedEnvelope: &CountEnvelope{SubscriptionID: "sub1", Count: ptr(int64(42))},
|
||||
},
|
||||
{
|
||||
Name: "COUNT envelope with count and hll",
|
||||
Message: []byte(`["COUNT","sub1",{"count":42, "hll": "0100000101000000000000040000000001020000000002000000000200000003000002040000000101020001010000000000000007000004010000000200040000020400000000000102000002000004010000010000000301000102030002000301000300010000070000000001000004000102010000000400010002000000000103000100010001000001040100020001000000000000010000020000000000030100000001000400010000000000000901010100000000040000000b030000010100010000010000010000000003000000000000010003000100020000000000010000010100000100000104000200030001000300000001000101000102"}]`),
|
||||
ExpectedEnvelope: &CountEnvelope{SubscriptionID: "sub1", Count: ptr(int64(42)), HyperLogLog: []byte{1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 2, 4, 0, 0, 0, 1, 1, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 4, 1, 0, 0, 0, 2, 0, 4, 0, 0, 2, 4, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 0, 0, 4, 1, 0, 0, 1, 0, 0, 0, 3, 1, 0, 1, 2, 3, 0, 2, 0, 3, 1, 0, 3, 0, 1, 0, 0, 7, 0, 0, 0, 0, 1, 0, 0, 4, 0, 1, 2, 1, 0, 0, 0, 4, 0, 1, 0, 2, 0, 0, 0, 0, 1, 3, 0, 1, 0, 1, 0, 1, 0, 0, 1, 4, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 1, 0, 4, 0, 1, 0, 0, 0, 0, 0, 0, 9, 1, 1, 1, 0, 0, 0, 0, 4, 0, 0, 0, 11, 3, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 4, 0, 2, 0, 3, 0, 1, 0, 3, 0, 0, 0, 1, 0, 1, 1, 0, 1, 2}},
|
||||
},
|
||||
{
|
||||
Name: "REQ envelope",
|
||||
Message: []byte(`["REQ","sub1", {"until": 999999, "kinds":[1]}]`),
|
||||
ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "sub1", Filters: Filters{{Kinds: []int{1}, Until: ptr(Timestamp(999999))}}},
|
||||
},
|
||||
{
|
||||
Name: "bigger REQ envelope",
|
||||
Message: []byte(`["REQ","sub1z\\\"zzz", {"authors":["9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a","8eee10b2ce1162b040fdcfdadb4f888c64aaf87531dab28cc0c09fbdea1b663e","0deadebefb3c1a760f036952abf675076343dd8424efdeaa0f1d9803a359ed46"],"since":1740425099,"limit":2,"#x":["<","as"]}, {"kinds": [2345, 112], "#plic": ["a"], "#ploc": ["blblb", "wuwuw"]}]`),
|
||||
ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "sub1z\\\"zzz", Filters: Filters{{Authors: []string{"9b86ca5d2a9b4aa09870e710438c2fd2fcdeca12a18b6f17ab9ebcdbc43f1d4a", "8eee10b2ce1162b040fdcfdadb4f888c64aaf87531dab28cc0c09fbdea1b663e", "0deadebefb3c1a760f036952abf675076343dd8424efdeaa0f1d9803a359ed46"}, Since: ptr(Timestamp(1740425099)), Limit: 2, Tags: TagMap{"x": []string{"<", "as"}}}, {Kinds: []int{2345, 112}, Tags: TagMap{"plic": []string{"a"}, "ploc": []string{"blblb", "wuwuw"}}}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
var pj *simdjson.ParsedJson
|
||||
envelope, err := ParseMessageSIMD(testCase.Message, pj)
|
||||
|
||||
if testCase.ExpectedErrorSubstring == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Contains(t, err.Error(), testCase.ExpectedErrorSubstring)
|
||||
return
|
||||
}
|
||||
|
||||
if testCase.ExpectedEnvelope == nil {
|
||||
require.Nil(t, envelope, "expected nil but got %v", envelope)
|
||||
return
|
||||
}
|
||||
|
||||
require.NotNil(t, envelope, "expected non-nil envelope but got nil")
|
||||
require.Equal(t, testCase.ExpectedEnvelope.Label(), envelope.Label())
|
||||
require.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[S any](s S) *S { return &s }
|
||||
|
84
event_simdjson.go
Normal file
84
event_simdjson.go
Normal file
@ -0,0 +1,84 @@
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/minio/simdjson-go"
|
||||
)
|
||||
|
||||
var (
|
||||
attrId = []byte("id")
|
||||
attrPubkey = []byte("pubkey")
|
||||
attrCreatedAt = []byte("created_at")
|
||||
attrKind = []byte("kind")
|
||||
attrContent = []byte("content")
|
||||
attrTags = []byte("tags")
|
||||
attrSig = []byte("sig")
|
||||
)
|
||||
|
||||
func (event *Event) UnmarshalSIMD(iter *simdjson.Iter) error {
|
||||
obj, err := iter.Object(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected at event: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
name, t, err := obj.NextElementBytes(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if t == simdjson.TypeNone {
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case slices.Equal(name, attrId):
|
||||
event.ID, err = iter.String()
|
||||
case slices.Equal(name, attrPubkey):
|
||||
event.PubKey, err = iter.String()
|
||||
case slices.Equal(name, attrContent):
|
||||
event.Content, err = iter.String()
|
||||
case slices.Equal(name, attrSig):
|
||||
event.Sig, err = iter.String()
|
||||
case slices.Equal(name, attrCreatedAt):
|
||||
var ts uint64
|
||||
ts, err = iter.Uint()
|
||||
event.CreatedAt = Timestamp(ts)
|
||||
case slices.Equal(name, attrKind):
|
||||
var kind uint64
|
||||
kind, err = iter.Uint()
|
||||
event.Kind = int(kind)
|
||||
case slices.Equal(name, attrTags):
|
||||
var arr *simdjson.Array
|
||||
arr, err = iter.Array(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.Tags = make(Tags, 0, 10)
|
||||
titer := arr.Iter()
|
||||
var subArr *simdjson.Array
|
||||
for {
|
||||
if t := titer.Advance(); t == simdjson.TypeNone {
|
||||
break
|
||||
}
|
||||
subArr, err = titer.Array(subArr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, err := subArr.AsString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.Tags = append(event.Tags, tag)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected event field '%s'", name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
107
filter_simdjson.go
Normal file
107
filter_simdjson.go
Normal file
@ -0,0 +1,107 @@
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/minio/simdjson-go"
|
||||
)
|
||||
|
||||
var (
|
||||
attrIds = []byte("ids")
|
||||
attrAuthors = []byte("authors")
|
||||
attrKinds = []byte("kinds")
|
||||
attrLimit = []byte("limit")
|
||||
attrSince = []byte("since")
|
||||
attrUntil = []byte("until")
|
||||
attrSearch = []byte("search")
|
||||
)
|
||||
|
||||
func (filter *Filter) UnmarshalSIMD(iter *simdjson.Iter) error {
|
||||
obj, err := iter.Object(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected at filter: %w", err)
|
||||
}
|
||||
|
||||
var arr *simdjson.Array
|
||||
for {
|
||||
name, t, err := obj.NextElementBytes(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if t == simdjson.TypeNone {
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case slices.Equal(name, attrIds):
|
||||
if arr, err = iter.Array(arr); err == nil {
|
||||
filter.IDs, err = arr.AsString()
|
||||
}
|
||||
case slices.Equal(name, attrAuthors):
|
||||
if arr, err = iter.Array(arr); err == nil {
|
||||
filter.Authors, err = arr.AsString()
|
||||
}
|
||||
case slices.Equal(name, attrKinds):
|
||||
if arr, err = iter.Array(arr); err == nil {
|
||||
i := arr.Iter()
|
||||
filter.Kinds = make([]int, 0, 6)
|
||||
for {
|
||||
t := i.Advance()
|
||||
if t == simdjson.TypeNone {
|
||||
break
|
||||
}
|
||||
if kind, err := i.Uint(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
filter.Kinds = append(filter.Kinds, int(kind))
|
||||
}
|
||||
}
|
||||
}
|
||||
case slices.Equal(name, attrSearch):
|
||||
filter.Search, err = iter.String()
|
||||
case slices.Equal(name, attrSince):
|
||||
var tsu uint64
|
||||
tsu, err = iter.Uint()
|
||||
ts := Timestamp(tsu)
|
||||
filter.Since = &ts
|
||||
case slices.Equal(name, attrUntil):
|
||||
var tsu uint64
|
||||
tsu, err = iter.Uint()
|
||||
ts := Timestamp(tsu)
|
||||
filter.Until = &ts
|
||||
case slices.Equal(name, attrLimit):
|
||||
var limit uint64
|
||||
limit, err = iter.Uint()
|
||||
filter.Limit = int(limit)
|
||||
if limit == 0 {
|
||||
filter.LimitZero = true
|
||||
}
|
||||
default:
|
||||
if len(name) > 1 && name[0] == '#' {
|
||||
if filter.Tags == nil {
|
||||
filter.Tags = make(TagMap, 1)
|
||||
}
|
||||
|
||||
arr, err := iter.Array(arr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vals, err := arr.AsString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter.Tags[string(name[1:])] = vals
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected filter field '%s'", name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
6
go.mod
6
go.mod
@ -59,9 +59,11 @@ require (
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/simdjson-go v0.4.5 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
@ -78,7 +80,7 @@ require (
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
|
8
go.sum
8
go.sum
@ -152,6 +152,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
|
||||
@ -165,6 +169,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/minio/simdjson-go v0.4.5 h1:r4IQwjRGmWCQ2VeMc7fGiilu1z5du0gJ/I/FsKwgo5A=
|
||||
github.com/minio/simdjson-go v0.4.5/go.mod h1:eoNz0DcLQRyEDeaPr4Ru6JpjlZPzbA0IodxVJk8lO8E=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -280,6 +286,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
Loading…
x
Reference in New Issue
Block a user