diff --git a/envelopes.go b/envelopes.go index 41ad2e2..c838e4e 100644 --- a/envelopes.go +++ b/envelopes.go @@ -164,7 +164,7 @@ func (v ReqEnvelope) MarshalJSON() ([]byte, error) { // CountEnvelope represents a COUNT message. type CountEnvelope struct { SubscriptionID string - Filters + Filter Count *int64 HyperLogLog []byte } @@ -198,12 +198,11 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error { return nil } - v.Filters = make(Filters, len(arr)-2) f := 0 for i := 2; i < len(arr); i++ { item := []byte(arr[i].Raw) - if err := easyjson.Unmarshal(item, &v.Filters[f]); err != nil { + if err := easyjson.Unmarshal(item, &v.Filter); err != nil { return fmt.Errorf("%w -- on filter %d", err, f) } @@ -217,9 +216,9 @@ func (v CountEnvelope) MarshalJSON() ([]byte, error) { w := jwriter.Writer{NoEscapeHTML: true} w.RawString(`["COUNT","`) w.RawString(v.SubscriptionID) - w.RawString(`"`) + w.RawString(`",`) if v.Count != nil { - w.RawString(`,{"count":`) + w.RawString(`{"count":`) w.RawString(strconv.FormatInt(*v.Count, 10)) if v.HyperLogLog != nil { w.RawString(`,"hll":"`) @@ -230,10 +229,7 @@ func (v CountEnvelope) MarshalJSON() ([]byte, error) { } w.RawString(`}`) } else { - for _, filter := range v.Filters { - w.RawString(`,`) - filter.MarshalEasyJSON(&w) - } + v.Filter.MarshalEasyJSON(&w) } w.RawString(`]`) return w.BuildBytes() diff --git a/envelopes_benchmark_test.go b/envelopes_benchmark_test.go index 323454e..797e141 100644 --- a/envelopes_benchmark_test.go +++ b/envelopes_benchmark_test.go @@ -11,59 +11,69 @@ import ( ) func BenchmarkParseMessage(b *testing.B) { - messages := generateTestMessages(2000) + for _, name := range []string{"relay", "client"} { + b.Run(name, func(b *testing.B) { + messages := generateTestMessages(name) - b.Run("stdlib", func(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, msg := range messages { - var v any - stdlibjson.Unmarshal(msg, &v) - } - } - }) + b.Run("jsonstdlib", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, msg := range messages { + var v any + stdlibjson.Unmarshal(msg, &v) + } + } + }) - b.Run("easyjson", func(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _ = ParseMessage(msg) - } - } - }) + b.Run("easyjson", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, msg := range messages { + _ = ParseMessage(msg) + } + } + }) - b.Run("simdjson", func(b *testing.B) { - smp := SIMDMessageParser{ParsedJSON: &simdjson.ParsedJson{}, AuxIter: &simdjson.Iter{}} - for i := 0; i < b.N; i++ { - for _, msg := range messages { - _, _ = smp.ParseMessage(msg) - } - } - }) + b.Run("simdjson", func(b *testing.B) { + smp := SIMDMessageParser{ParsedJSON: &simdjson.ParsedJson{}, AuxIter: &simdjson.Iter{}} + for i := 0; i < b.N; i++ { + for _, msg := range messages { + _, _ = smp.ParseMessage(msg) + } + } + }) + + b.Run("sonic", func(b *testing.B) { + smp := SonicMessageParser{} + for i := 0; i < b.N; i++ { + for _, msg := range messages { + _, _ = smp.ParseMessage(msg) + } + } + }) + }) + } } -func generateTestMessages(count int) [][]byte { - messages := make([][]byte, 0, count) +func generateTestMessages(typ string) [][]byte { + messages := make([][]byte, 0, 600) - for i := 0; i < count; i++ { - var msg []byte + setup := map[string]map[int]func() []byte{ + "client": { + 500: generateEventMessage, + 5: generateEOSEMessage, + 9: generateNoticeMessage, + 14: generateCountMessage, + 20: generateOKMessage, + }, + "relay": { + 500: generateReqMessage, + 10: generateCountMessage, + }, + }[typ] - switch rand.IntN(12) { - case 1: - msg = generateAuthMessage() - case 2: - msg = generateNoticeMessage() - case 3: - msg = generateEOSEMessage() - case 4: - msg = generateOKMessage() - case 5: - msg = generateCountMessage() - case 6: - msg = generateReqMessage() - default: - msg = generateEventMessage() + for count, generator := range setup { + for range count { + messages = append(messages, generator()) } - - messages = append(messages, msg) } return messages diff --git a/envelopes_simdjson.go b/envelopes_simdjson.go index 72f8a2b..22eed23 100644 --- a/envelopes_simdjson.go +++ b/envelopes_simdjson.go @@ -129,13 +129,11 @@ func (smp *SIMDMessageParser) ParseMessage(message []byte) (Envelope, error) { } } } else { - var filter Filter - smp.TargetObject, smp.TargetInternalArray, err = filter.UnmarshalSIMD( + smp.TargetObject, smp.TargetInternalArray, err = v.Filter.UnmarshalSIMD( &iter, smp.TargetObject, smp.TargetInternalArray) if err != nil { return nil, err } - v.Filters = Filters{filter} } return v, nil @@ -208,3 +206,184 @@ func (smp *SIMDMessageParser) ParseMessage(message []byte) (Envelope, error) { return nil, UnknownLabel } } + +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, + obj *simdjson.Object, + arr *simdjson.Array, + subArr *simdjson.Array, +) (*simdjson.Object, *simdjson.Array, *simdjson.Array, error) { + obj, err := iter.Object(obj) + if err != nil { + return obj, arr, subArr, fmt.Errorf("unexpected at event: %w", err) + } + + for { + name, t, err := obj.NextElementBytes(iter) + if err != nil { + return obj, arr, subArr, err + } else if t == simdjson.TypeNone { + break + } + + switch { + case bytes.Equal(name, attrId): + event.ID, err = iter.String() + case bytes.Equal(name, attrPubkey): + event.PubKey, err = iter.String() + case bytes.Equal(name, attrContent): + event.Content, err = iter.String() + case bytes.Equal(name, attrSig): + event.Sig, err = iter.String() + case bytes.Equal(name, attrCreatedAt): + var ts uint64 + ts, err = iter.Uint() + event.CreatedAt = Timestamp(ts) + case bytes.Equal(name, attrKind): + var kind uint64 + kind, err = iter.Uint() + event.Kind = int(kind) + case bytes.Equal(name, attrTags): + arr, err = iter.Array(arr) + if err != nil { + return obj, arr, subArr, err + } + event.Tags = make(Tags, 0, 10) + titer := arr.Iter() + for { + if t := titer.Advance(); t == simdjson.TypeNone { + break + } + subArr, err = titer.Array(subArr) + if err != nil { + return obj, arr, subArr, err + } + tag, err := subArr.AsString() + if err != nil { + return obj, arr, subArr, err + } + event.Tags = append(event.Tags, tag) + } + default: + return obj, arr, subArr, fmt.Errorf("unexpected event field '%s'", name) + } + + if err != nil { + return obj, arr, subArr, err + } + } + + return obj, arr, subArr, nil +} + +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, + obj *simdjson.Object, + arr *simdjson.Array, +) (*simdjson.Object, *simdjson.Array, error) { + obj, err := iter.Object(obj) + if err != nil { + return obj, arr, fmt.Errorf("unexpected at filter: %w", err) + } + + for { + name, t, err := obj.NextElementBytes(iter) + if err != nil { + return obj, arr, err + } else if t == simdjson.TypeNone { + break + } + + switch { + case bytes.Equal(name, attrIds): + if arr, err = iter.Array(arr); err == nil { + filter.IDs, err = arr.AsString() + } + case bytes.Equal(name, attrAuthors): + if arr, err = iter.Array(arr); err == nil { + filter.Authors, err = arr.AsString() + } + case bytes.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 obj, arr, err + } else { + filter.Kinds = append(filter.Kinds, int(kind)) + } + } + } + case bytes.Equal(name, attrSearch): + filter.Search, err = iter.String() + case bytes.Equal(name, attrSince): + var tsu uint64 + tsu, err = iter.Uint() + ts := Timestamp(tsu) + filter.Since = &ts + case bytes.Equal(name, attrUntil): + var tsu uint64 + tsu, err = iter.Uint() + ts := Timestamp(tsu) + filter.Until = &ts + case bytes.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 obj, arr, err + } + vals, err := arr.AsString() + if err != nil { + return obj, arr, err + } + + filter.Tags[string(name[1:])] = vals + continue + } + + return obj, arr, fmt.Errorf("unexpected filter field '%s'", name) + } + + if err != nil { + return obj, arr, err + } + } + + return obj, arr, nil +} diff --git a/envelopes_sonic.go b/envelopes_sonic.go new file mode 100644 index 0000000..013bc35 --- /dev/null +++ b/envelopes_sonic.go @@ -0,0 +1,185 @@ +package nostr + +import ( + "encoding/hex" + "fmt" + + "github.com/bytedance/sonic/ast" +) + +type SonicMessageParser struct{} + +func (smp *SonicMessageParser) ParseMessage(message []byte) (Envelope, error) { + var err error + + tlarr, _ := ast.NewParser(string(message)).Parse() + label, _ := tlarr.Index(0).StrictString() + + var v Envelope + switch label { + case "EVENT": + env := &EventEnvelope{} + sndN := tlarr.Index(1) + var evtN *ast.Node + switch sndN.TypeSafe() { + case ast.V_STRING: + subId, _ := sndN.StrictString() + env.SubscriptionID = &subId + evtN = tlarr.Index(2) + case ast.V_OBJECT: + evtN = sndN + } + err = eventFromSonicAst(&env.Event, evtN) + v = env + case "REQ": + env := &ReqEnvelope{} + nodes, _ := tlarr.ArrayUseNode() + env.SubscriptionID, _ = nodes[1].StrictString() + env.Filters = make(Filters, len(nodes)-2) + for i, node := range nodes[2:] { + err = filterFromSonicAst(&env.Filters[i], &node) + } + v = env + case "COUNT": + env := &CountEnvelope{} + env.SubscriptionID, _ = tlarr.Index(1).StrictString() + trdN := tlarr.Index(2) + if countN := trdN.Get("count"); countN.Exists() { + count, _ := countN.Int64() + env.Count = &count + hll, _ := trdN.Get("hll").StrictString() + if len(hll) == 512 { + env.HyperLogLog, _ = hex.DecodeString(hll) + } + } else { + err = filterFromSonicAst(&env.Filter, trdN) + } + v = env + case "NOTICE": + notice, _ := tlarr.Index(1).StrictString() + env := NoticeEnvelope(notice) + v = &env + case "EOSE": + subId, _ := tlarr.Index(1).StrictString() + env := EOSEEnvelope(subId) + v = &env + case "OK": + env := &OKEnvelope{} + env.EventID, _ = tlarr.Index(1).StrictString() + env.OK, _ = tlarr.Index(2).Bool() + env.Reason, _ = tlarr.Index(3).StrictString() + v = env + case "AUTH": + env := &AuthEnvelope{} + sndN := tlarr.Index(1) + switch sndN.TypeSafe() { + case ast.V_STRING: + challenge, _ := sndN.StrictString() + env.Challenge = &challenge + case ast.V_OBJECT: + err = eventFromSonicAst(&env.Event, sndN) + } + v = env + case "CLOSED": + env := &ClosedEnvelope{} + env.SubscriptionID, _ = tlarr.Index(1).StrictString() + env.Reason, _ = tlarr.Index(2).StrictString() + v = env + case "CLOSE": + reason, _ := tlarr.Index(1).StrictString() + env := CloseEnvelope(reason) + v = &env + default: + return nil, UnknownLabel + } + + return v, err +} + +func eventFromSonicAst(evt *Event, node *ast.Node) error { + evt.ID, _ = node.Get("id").StrictString() + evt.PubKey, _ = node.Get("pubkey").StrictString() + evt.Content, _ = node.Get("content").StrictString() + evt.Sig, _ = node.Get("sig").StrictString() + kind, _ := node.Get("kind").Int64() + evt.Kind = int(kind) + createdAt, _ := node.Get("created_at").Int64() + evt.CreatedAt = Timestamp(createdAt) + tagsN, err := node.Get("tags").ArrayUseNode() + if err != nil { + return fmt.Errorf("invalid tags: %w", err) + } + evt.Tags = make(Tags, len(tagsN)) + for i, tagN := range tagsN { + itemsN, err := tagN.ArrayUseNode() + if err != nil { + return fmt.Errorf("invalid tag: %w", err) + } + tag := make(Tag, len(itemsN)) + + for j, itemN := range itemsN { + tag[j], _ = itemN.StrictString() + } + + evt.Tags[i] = tag + } + return nil +} + +func filterFromSonicAst(filter *Filter, node *ast.Node) error { + var err error + + node.ForEach(func(path ast.Sequence, node *ast.Node) bool { + switch *path.Key { + case "limit": + limit, _ := node.Int64() + filter.Limit = int(limit) + filter.LimitZero = filter.Limit == 0 + case "since": + since, _ := node.Int64() + filter.Since = (*Timestamp)(&since) + case "until": + until, _ := node.Int64() + filter.Until = (*Timestamp)(&until) + case "search": + filter.Search, _ = node.StrictString() + case "ids": + idsN, _ := node.ArrayUseNode() + filter.IDs = make([]string, len(idsN)) + for i, idN := range idsN { + filter.IDs[i], _ = idN.StrictString() + } + case "authors": + authorsN, _ := node.ArrayUseNode() + filter.Authors = make([]string, len(authorsN)) + for i, authorN := range authorsN { + filter.Authors[i], _ = authorN.StrictString() + } + case "kinds": + kindsN, _ := node.ArrayUseNode() + filter.Kinds = make([]int, len(kindsN)) + for i, kindN := range kindsN { + kind, _ := kindN.Int64() + filter.Kinds[i] = int(kind) + } + default: + if len(*path.Key) > 1 && (*path.Key)[0] == '#' { + if filter.Tags == nil { + filter.Tags = make(TagMap, 2) + } + tagsN, _ := node.ArrayUseNode() + tags := make([]string, len(tagsN)) + for i, authorN := range tagsN { + tags[i], _ = authorN.StrictString() + } + filter.Tags[(*path.Key)[1:]] = tags + } else { + err = fmt.Errorf("unexpected field '%s'", *path.Key) + return false + } + } + return true + }) + + return err +} diff --git a/envelopes_test.go b/envelopes_test.go index 91410e1..6bb5928 100644 --- a/envelopes_test.go +++ b/envelopes_test.go @@ -8,120 +8,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestEventEnvelopeEncodingAndDecoding(t *testing.T) { - eventEnvelopes := []string{ - `["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"}]`, - `["EVENT",{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["p","75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"],["p","46d0dfd3a724a302ca9175163bdf788f3606b3fd1bb12d5fe055d1e418cb60ea"]],"content":"{\"wss://nostr-pub.wellorder.net\":{\"read\":true,\"write\":true},\"wss://nostr.bitcoiner.social\":{\"read\":false,\"write\":true},\"wss://expensive-relay.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relayer.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relay.bitid.nz\":{\"read\":true,\"write\":true},\"wss://nostr.rocks\":{\"read\":true,\"write\":true}}","sig":"811355d3484d375df47581cb5d66bed05002c2978894098304f20b595e571b7e01b2efd906c5650080ffe49cf1c62b36715698e9d88b9e8be43029a2f3fa66be"}]`, - } - - for _, raw := range eventEnvelopes { - var env EventEnvelope - err := json.Unmarshal([]byte(raw), &env) - assert.NoError(t, err) - assert.Equal(t, env.GetID(), env.ID) - - ok, _ := env.CheckSignature() - assert.True(t, ok) - - asJSON, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, raw, string(asJSON)) - } -} - -func TestNoticeEnvelopeEncodingAndDecoding(t *testing.T) { - noticeEnv := `["NOTICE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]` - var env NoticeEnvelope - err := json.Unmarshal([]byte(noticeEnv), &env) - assert.NoError(t, err) - assert.Equal(t, "kjasbdlasvdluiasvd\"kjasbdksab\\d", string(env)) - - res, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, noticeEnv, string(res)) -} - -func TestEoseEnvelopeEncodingAndDecoding(t *testing.T) { - eoseEnv := `["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]` - var env EOSEEnvelope - err := json.Unmarshal([]byte(eoseEnv), &env) - assert.NoError(t, err) - assert.Equal(t, "kjasbdlasvdluiasvd\"kjasbdksab\\d", string(env)) - - res, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, eoseEnv, string(res)) -} - -func TestCountEnvelopeEncodingAndDecoding(t *testing.T) { - countEnv := `["COUNT","z",{"count":12}]` - var env CountEnvelope - err := json.Unmarshal([]byte(countEnv), &env) - assert.NoError(t, err) - assert.Equal(t, int64(12), *env.Count) - - res, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, countEnv, string(res)) -} - -func TestOKEnvelopeEncodingAndDecoding(t *testing.T) { - okEnvelopes := []string{ - `["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",false,"error: could not connect to the database"]`, - `["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`, - } - - for _, raw := range okEnvelopes { - var env OKEnvelope - err := json.Unmarshal([]byte(raw), &env) - assert.NoError(t, err) - - asJSON, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, raw, string(asJSON)) - } -} - -func TestClosedEnvelopeEncodingAndDecoding(t *testing.T) { - closeEnvelopes := []string{ - `["CLOSED","_","error: something went wrong"]`, - `["CLOSED",":1","auth-required: take a selfie and send it to the CIA"]`, - } - - for _, raw := range closeEnvelopes { - var env ClosedEnvelope - err := json.Unmarshal([]byte(raw), &env) - assert.NoError(t, err) - assert.Condition(t, func() (success bool) { - if env.SubscriptionID != "_" && env.SubscriptionID != ":1" { - return false - } - return true - }) - - res, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, raw, string(res)) - } -} - -func TestAuthEnvelopeEncodingAndDecoding(t *testing.T) { - authEnvelopes := []string{ - `["AUTH","kjsabdlasb aslkd kasndkad \"as.kdnbskadb"]`, - `["AUTH",{"kind":1,"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1683660344,"tags":[],"content":"hello world","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}]`, - } - - for _, raw := range authEnvelopes { - var env AuthEnvelope - err := json.Unmarshal([]byte(raw), &env) - assert.NoError(t, err) - - asJSON, err := json.Marshal(env) - assert.NoError(t, err) - assert.Equal(t, raw, string(asJSON)) - } -} - func TestParseMessage(t *testing.T) { testCases := []struct { Name string @@ -144,187 +30,142 @@ func TestParseMessage(t *testing.T) { ExpectedEnvelope: nil, }, { - Name: "CLOSED envelope", - Message: []byte(`["CLOSED",":1","error: we are broken"]`), - ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: ":1", Reason: "error: we are broken"}, + 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: "AUTH envelope", - Message: []byte(`["AUTH","bisteka"]`), - ExpectedEnvelope: &AuthEnvelope{Challenge: ptr("bisteka")}, + 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: "EVENT envelope with tags", + Message: []byte(`["EVENT",{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["p","75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"],["p","46d0dfd3a724a302ca9175163bdf788f3606b3fd1bb12d5fe055d1e418cb60ea"]],"content":"{\"wss://nostr-pub.wellorder.net\":{\"read\":true,\"write\":true},\"wss://nostr.bitcoiner.social\":{\"read\":false,\"write\":true},\"wss://expensive-relay.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relayer.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relay.bitid.nz\":{\"read\":true,\"write\":true},\"wss://nostr.rocks\":{\"read\":true,\"write\":true}}","sig":"811355d3484d375df47581cb5d66bed05002c2978894098304f20b595e571b7e01b2efd906c5650080ffe49cf1c62b36715698e9d88b9e8be43029a2f3fa66be"}]`), + ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 3, ID: "9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2", PubKey: "373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7", CreatedAt: 1644844224, Tags: Tags{Tag{"p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}, Tag{"p", "75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"}, Tag{"p", "46d0dfd3a724a302ca9175163bdf788f3606b3fd1bb12d5fe055d1e418cb60ea"}}, Content: "{\"wss://nostr-pub.wellorder.net\":{\"read\":true,\"write\":true},\"wss://nostr.bitcoiner.social\":{\"read\":false,\"write\":true},\"wss://expensive-relay.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relayer.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relay.bitid.nz\":{\"read\":true,\"write\":true},\"wss://nostr.rocks\":{\"read\":true,\"write\":true}}", Sig: "811355d3484d375df47581cb5d66bed05002c2978894098304f20b595e571b7e01b2efd906c5650080ffe49cf1c62b36715698e9d88b9e8be43029a2f3fa66be"}}, + }, + { + Name: "NOTICE envelope", + Message: []byte(`["NOTICE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`), + ExpectedEnvelope: ptr(NoticeEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")), + }, + { + Name: "EOSE envelope", + Message: []byte(`["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`), + ExpectedEnvelope: ptr(EOSEEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")), + }, + { + Name: "COUNT envelope", + Message: []byte(`["COUNT","z",{"count":12}]`), + ExpectedEnvelope: &CountEnvelope{SubscriptionID: "z", Count: ptr(int64(12))}, + }, + { + Name: "COUNT envelope with 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: "OK envelope success", + Message: []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`), + ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: true, Reason: ""}, + }, + { + Name: "OK envelope failure", + Message: []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",false,"error: could not connect to the database"]`), + ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: false, Reason: "error: could not connect to the database"}, + }, + { + Name: "CLOSED envelope with underscore", + Message: []byte(`["CLOSED","_","error: something went wrong"]`), + ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: "_", Reason: "error: something went wrong"}, + }, + { + Name: "CLOSED envelope with colon", + Message: []byte(`["CLOSED",":1","auth-required: take a selfie and send it to the CIA"]`), + ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: ":1", Reason: "auth-required: take a selfie and send it to the CIA"}, + }, + { + Name: "AUTH envelope with challenge", + Message: []byte(`["AUTH","kjsabdlasb aslkd kasndkad \"as.kdnbskadb"]`), + ExpectedEnvelope: &AuthEnvelope{Challenge: ptr("kjsabdlasb aslkd kasndkad \"as.kdnbskadb")}, + }, + { + Name: "AUTH envelope with event", + Message: []byte(`["AUTH",{"kind":1,"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1683660344,"tags":[],"content":"hello world","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}]`), + ExpectedEnvelope: &AuthEnvelope{Event: Event{Kind: 1, ID: "ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69", PubKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", CreatedAt: 1683660344, Tags: Tags{}, Content: "hello world", Sig: "94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}}, }, { Name: "REQ envelope", Message: []byte(`["REQ","million", {"kinds": [1]}, {"kinds": [30023 ], "#d": ["buteko", "batuke"]}]`), ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "million", Filters: Filters{{Kinds: []int{1}}, {Kinds: []int{30023}, Tags: TagMap{"d": []string{"buteko", "batuke"}}}}}, }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - envelope := ParseMessage(testCase.Message) - if testCase.ExpectedEnvelope == nil && envelope == nil { - return - } - - if testCase.ExpectedEnvelope == nil { - assert.NotNil(t, envelope, "expected nil but got %v\n", envelope) - } - - assert.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String()) - }) - } -} - -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("standard", func(t *testing.T) { + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + envelope := ParseMessage(testCase.Message) + if testCase.ExpectedEnvelope == nil && envelope == nil { + return + } + + if testCase.ExpectedEnvelope == nil { + assert.Nil(t, envelope, "expected nil but got %v", envelope) + return + } + + assert.NotNil(t, envelope, "expected non-nil envelope but got nil") + assert.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String()) + }) + } + }) + + t.Run("simdjson", func(t *testing.T) { smp := SIMDMessageParser{AuxIter: &simdjson.Iter{}} + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + envelope, err := smp.ParseMessage(testCase.Message) - t.Run(testCase.Name, func(t *testing.T) { - envelope, err := smp.ParseMessage(testCase.Message) + if testCase.ExpectedEnvelope == nil && envelope == nil { + return + } + + if testCase.ExpectedEnvelope == nil { + require.Nil(t, envelope, "expected nil but got %v", envelope) + return + } - if testCase.ExpectedErrorSubstring == "" { require.NoError(t, err) - } else { - require.Contains(t, err.Error(), testCase.ExpectedErrorSubstring) - return - } + require.NotNil(t, envelope, "expected non-nil envelope but got nil") + require.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String()) + }) + } + }) - if testCase.ExpectedEnvelope == nil { - require.Nil(t, envelope, "expected nil but got %v", envelope) - return - } + t.Run("sonic", func(t *testing.T) { + smp := SonicMessageParser{} + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + envelope, err := smp.ParseMessage(testCase.Message) - require.NotNil(t, envelope, "expected non-nil envelope but got nil") - require.Equal(t, testCase.ExpectedEnvelope, envelope) - }) - } + if testCase.ExpectedEnvelope == nil && envelope == nil { + return + } + + if testCase.ExpectedEnvelope == nil { + require.Nil(t, envelope, "expected nil but got %v", envelope) + return + } + + require.NoError(t, err) + require.NotNil(t, envelope, "expected non-nil envelope but got nil") + require.Equal(t, testCase.ExpectedEnvelope.String(), envelope.String()) + }) + } + }) } - -func ptr[S any](s S) *S { return &s } diff --git a/event_simdjson.go b/event_simdjson.go deleted file mode 100644 index a3d7cd8..0000000 --- a/event_simdjson.go +++ /dev/null @@ -1,87 +0,0 @@ -package nostr - -import ( - "bytes" - "fmt" - - "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, - obj *simdjson.Object, - arr *simdjson.Array, - subArr *simdjson.Array, -) (*simdjson.Object, *simdjson.Array, *simdjson.Array, error) { - obj, err := iter.Object(obj) - if err != nil { - return obj, arr, subArr, fmt.Errorf("unexpected at event: %w", err) - } - - for { - name, t, err := obj.NextElementBytes(iter) - if err != nil { - return obj, arr, subArr, err - } else if t == simdjson.TypeNone { - break - } - - switch { - case bytes.Equal(name, attrId): - event.ID, err = iter.String() - case bytes.Equal(name, attrPubkey): - event.PubKey, err = iter.String() - case bytes.Equal(name, attrContent): - event.Content, err = iter.String() - case bytes.Equal(name, attrSig): - event.Sig, err = iter.String() - case bytes.Equal(name, attrCreatedAt): - var ts uint64 - ts, err = iter.Uint() - event.CreatedAt = Timestamp(ts) - case bytes.Equal(name, attrKind): - var kind uint64 - kind, err = iter.Uint() - event.Kind = int(kind) - case bytes.Equal(name, attrTags): - arr, err = iter.Array(arr) - if err != nil { - return obj, arr, subArr, err - } - event.Tags = make(Tags, 0, 10) - titer := arr.Iter() - for { - if t := titer.Advance(); t == simdjson.TypeNone { - break - } - subArr, err = titer.Array(subArr) - if err != nil { - return obj, arr, subArr, err - } - tag, err := subArr.AsString() - if err != nil { - return obj, arr, subArr, err - } - event.Tags = append(event.Tags, tag) - } - default: - return obj, arr, subArr, fmt.Errorf("unexpected event field '%s'", name) - } - - if err != nil { - return obj, arr, subArr, err - } - } - - return obj, arr, subArr, nil -} diff --git a/filter_simdjson.go b/filter_simdjson.go deleted file mode 100644 index 700a1dc..0000000 --- a/filter_simdjson.go +++ /dev/null @@ -1,110 +0,0 @@ -package nostr - -import ( - "bytes" - "fmt" - - "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, - obj *simdjson.Object, - arr *simdjson.Array, -) (*simdjson.Object, *simdjson.Array, error) { - obj, err := iter.Object(obj) - if err != nil { - return obj, arr, fmt.Errorf("unexpected at filter: %w", err) - } - - for { - name, t, err := obj.NextElementBytes(iter) - if err != nil { - return obj, arr, err - } else if t == simdjson.TypeNone { - break - } - - switch { - case bytes.Equal(name, attrIds): - if arr, err = iter.Array(arr); err == nil { - filter.IDs, err = arr.AsString() - } - case bytes.Equal(name, attrAuthors): - if arr, err = iter.Array(arr); err == nil { - filter.Authors, err = arr.AsString() - } - case bytes.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 obj, arr, err - } else { - filter.Kinds = append(filter.Kinds, int(kind)) - } - } - } - case bytes.Equal(name, attrSearch): - filter.Search, err = iter.String() - case bytes.Equal(name, attrSince): - var tsu uint64 - tsu, err = iter.Uint() - ts := Timestamp(tsu) - filter.Since = &ts - case bytes.Equal(name, attrUntil): - var tsu uint64 - tsu, err = iter.Uint() - ts := Timestamp(tsu) - filter.Until = &ts - case bytes.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 obj, arr, err - } - vals, err := arr.AsString() - if err != nil { - return obj, arr, err - } - - filter.Tags[string(name[1:])] = vals - continue - } - - return obj, arr, fmt.Errorf("unexpected filter field '%s'", name) - } - - if err != nil { - return obj, arr, err - } - } - - return obj, arr, nil -} diff --git a/go.mod b/go.mod index 48fc039..fff5bfd 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bluekeyes/go-gitdiff v0.7.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/bytedance/sonic v1.12.10 github.com/coder/websocket v1.8.12 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/dgraph-io/badger/v4 v4.5.0 @@ -22,6 +23,7 @@ require ( github.com/mailru/easyjson v0.7.7 github.com/mattn/go-sqlite3 v1.14.24 github.com/microcosm-cc/bluemonday v1.0.27 + github.com/minio/simdjson-go v0.4.5 github.com/ncruces/go-sqlite3 v0.18.3 github.com/puzpuzpuz/xsync/v3 v3.4.0 github.com/stretchr/testify v1.10.0 @@ -29,6 +31,7 @@ require ( github.com/tursodatabase/go-libsql v0.0.0-20240916111504-922dfa87e1e6 github.com/tyler-smith/go-bip32 v1.0.0 github.com/tyler-smith/go-bip39 v1.1.0 + github.com/valyala/fasthttp v1.51.0 golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d golang.org/x/net v0.34.0 @@ -46,7 +49,9 @@ require ( github.com/bep/debounce v1.2.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect @@ -63,7 +68,6 @@ require ( 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 @@ -76,11 +80,12 @@ require ( github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - 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.30.0 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/sys v0.31.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 diff --git a/go.sum b/go.sum index 3ffea0b..987e28c 100644 --- a/go.sum +++ b/go.sum @@ -49,10 +49,18 @@ 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/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/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= +github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -150,12 +158,12 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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= @@ -228,6 +236,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tursodatabase/go-libsql v0.0.0-20240916111504-922dfa87e1e6 h1:bFxO2fsY5mHZRrVvhmrAo/O8Agi9HDAIMmmOClZMrkQ= github.com/tursodatabase/go-libsql v0.0.0-20240916111504-922dfa87e1e6/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= @@ -240,6 +250,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -284,10 +296,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -365,3 +375,4 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/helpers_test.go b/helpers_test.go index b662b64..88d248e 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -42,3 +42,5 @@ func TestSubIdExtract(t *testing.T) { require.Equal(t, "xxz", extractSubID(data)) } } + +func ptr[S any](s S) *S { return &s } diff --git a/subscription.go b/subscription.go index 807de06..e6d7e1c 100644 --- a/subscription.go +++ b/subscription.go @@ -167,8 +167,10 @@ func (sub *Subscription) Fire() error { var reqb []byte if sub.countResult == nil { reqb, _ = ReqEnvelope{sub.id, sub.Filters}.MarshalJSON() + } else if len(sub.Filters) == 1 { + reqb, _ = CountEnvelope{sub.id, sub.Filters[0], nil, nil}.MarshalJSON() } else { - reqb, _ = CountEnvelope{sub.id, sub.Filters, nil, nil}.MarshalJSON() + return fmt.Errorf("unexpected sub configuration") } sub.live.Store(true)