fix and improve envelope stuff again, deal with messages as strings on all envelope parsing steps.

This commit is contained in:
fiatjaf 2025-03-12 00:17:01 -03:00
parent 42a2243b72
commit cecc71cd81
11 changed files with 151 additions and 140 deletions

View File

@ -1,81 +1,75 @@
package nostr
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"unsafe"
"github.com/mailru/easyjson"
jwriter "github.com/mailru/easyjson/jwriter"
"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")
)
var UnknownLabel = errors.New("unknown envelope label")
type MessageParser interface {
// ParseMessage parses a message into an Envelope.
ParseMessage([]byte) (Envelope, error)
ParseMessage(string) (Envelope, error)
}
// Deprecated: use NewMessageParser instead
func ParseMessage(message []byte) Envelope {
firstComma := bytes.Index(message, []byte{','})
if firstComma == -1 {
func ParseMessage(message string) Envelope {
firstQuote := strings.IndexRune(message, '"')
if firstQuote == -1 {
return nil
}
label := message[0:firstComma]
secondQuote := strings.IndexRune(message[firstQuote+1:], '"')
if secondQuote == -1 {
return nil
}
label := message[firstQuote+1 : firstQuote+1+secondQuote]
var v Envelope
switch {
case bytes.Contains(label, labelEvent):
switch label {
case "EVENT":
v = &EventEnvelope{}
case bytes.Contains(label, labelReq):
case "REQ":
v = &ReqEnvelope{}
case bytes.Contains(label, labelCount):
case "COUNT":
v = &CountEnvelope{}
case bytes.Contains(label, labelNotice):
case "NOTICE":
x := NoticeEnvelope("")
v = &x
case bytes.Contains(label, labelEose):
case "EOSE":
x := EOSEEnvelope("")
v = &x
case bytes.Contains(label, labelOk):
case "OK":
v = &OKEnvelope{}
case bytes.Contains(label, labelAuth):
case "AUTH":
v = &AuthEnvelope{}
case bytes.Contains(label, labelClosed):
case "CLOSED":
v = &ClosedEnvelope{}
case bytes.Contains(label, labelClose):
case "CLOSE":
x := CloseEnvelope("")
v = &x
default:
return nil
}
if err := v.UnmarshalJSON(message); err != nil {
if err := v.FromJSON(message); err != nil {
return nil
}
return v
}
// Envelope is the interface for all nostr message envelopes.
type Envelope interface {
Label() string
UnmarshalJSON([]byte) error
FromJSON(string) error
MarshalJSON() ([]byte, error)
String() string
}
@ -99,15 +93,15 @@ type EventEnvelope struct {
func (_ EventEnvelope) Label() string { return "EVENT" }
func (v *EventEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *EventEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
switch len(arr) {
case 2:
return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event)
return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[1].Raw), len(arr[1].Raw)), &v.Event)
case 3:
v.SubscriptionID = &arr[1].Str
return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Event)
return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &v.Event)
default:
return fmt.Errorf("failed to decode EVENT envelope")
}
@ -134,8 +128,8 @@ type ReqEnvelope struct {
func (_ ReqEnvelope) Label() string { return "REQ" }
func (v *ReqEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *ReqEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 3 {
return fmt.Errorf("failed to decode REQ envelope: missing filters")
@ -144,7 +138,7 @@ func (v *ReqEnvelope) UnmarshalJSON(data []byte) error {
v.Filters = make(Filters, len(arr)-2)
f := 0
for i := 2; i < len(arr); i++ {
if err := easyjson.Unmarshal([]byte(arr[i].Raw), &v.Filters[f]); err != nil {
if err := easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[i].Raw), len(arr[i].Raw)), &v.Filters[f]); err != nil {
return fmt.Errorf("%w -- on filter %d", err, f)
}
f++
@ -180,8 +174,8 @@ func (c CountEnvelope) String() string {
return string(v)
}
func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *CountEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 3 {
return fmt.Errorf("failed to decode COUNT envelope: missing filters")
@ -192,7 +186,7 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
Count *int64 `json:"count"`
HLL string `json:"hll"`
}
if err := json.Unmarshal([]byte(arr[2].Raw), &countResult); err == nil && countResult.Count != nil {
if err := json.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &countResult); err == nil && countResult.Count != nil {
v.Count = countResult.Count
if len(countResult.HLL) == 512 {
v.HyperLogLog, err = hex.DecodeString(countResult.HLL)
@ -205,7 +199,7 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
f := 0
for i := 2; i < len(arr); i++ {
item := []byte(arr[i].Raw)
item := unsafe.Slice(unsafe.StringData(arr[i].Raw), len(arr[i].Raw))
if err := easyjson.Unmarshal(item, &v.Filter); err != nil {
return fmt.Errorf("%w -- on filter %d", err, f)
@ -249,8 +243,8 @@ func (n NoticeEnvelope) String() string {
return string(v)
}
func (v *NoticeEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *NoticeEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 2 {
return fmt.Errorf("failed to decode NOTICE envelope")
@ -276,8 +270,8 @@ func (e EOSEEnvelope) String() string {
return string(v)
}
func (v *EOSEEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *EOSEEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 2 {
return fmt.Errorf("failed to decode EOSE envelope")
@ -303,8 +297,8 @@ func (c CloseEnvelope) String() string {
return string(v)
}
func (v *CloseEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *CloseEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
switch len(arr) {
case 2:
@ -335,8 +329,8 @@ func (c ClosedEnvelope) String() string {
return string(v)
}
func (v *ClosedEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *ClosedEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
switch len(arr) {
case 3:
@ -370,8 +364,8 @@ func (o OKEnvelope) String() string {
return string(v)
}
func (v *OKEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *OKEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 4 {
return fmt.Errorf("failed to decode OK envelope: missing fields")
@ -411,14 +405,14 @@ func (a AuthEnvelope) String() string {
return string(v)
}
func (v *AuthEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *AuthEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 2 {
return fmt.Errorf("failed to decode Auth envelope: missing fields")
}
if arr[1].IsObject() {
return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event)
return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[1].Raw), len(arr[1].Raw)), &v.Event)
} else {
v.Challenge = &arr[1].Str
}

View File

@ -1,3 +1,5 @@
//go:build sonic
package nostr
import (
@ -6,6 +8,7 @@ import (
"math/rand/v2"
"testing"
"time"
"unsafe"
)
func BenchmarkParseMessage(b *testing.B) {
@ -17,7 +20,7 @@ func BenchmarkParseMessage(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, msg := range messages {
var v any
stdlibjson.Unmarshal(msg, &v)
stdlibjson.Unmarshal(unsafe.Slice(unsafe.StringData(msg), len(msg)), &v)
}
}
})
@ -42,8 +45,8 @@ func BenchmarkParseMessage(b *testing.B) {
}
}
func generateTestMessages(typ string) [][]byte {
messages := make([][]byte, 0, 600)
func generateTestMessages(typ string) []string {
messages := make([]string, 0, 600)
setup := map[string]map[int]func() []byte{
"client": {
@ -62,7 +65,7 @@ func generateTestMessages(typ string) [][]byte {
for count, generator := range setup {
for range count {
messages = append(messages, generator())
messages = append(messages, string(generator()))
}
}

View File

@ -3,8 +3,8 @@
package nostr
import (
"bytes"
"errors"
"strings"
)
func NewMessageParser() MessageParser {
@ -13,41 +13,45 @@ func NewMessageParser() MessageParser {
type messageParser struct{}
func (messageParser) ParseMessage(message []byte) (Envelope, error) {
firstComma := bytes.Index(message, []byte{','})
if firstComma == -1 {
func (messageParser) ParseMessage(message string) (Envelope, error) {
firstQuote := strings.IndexRune(message, '"')
if firstQuote == -1 {
return nil, errors.New("malformed json")
}
label := message[0:firstComma]
secondQuote := strings.IndexRune(message[firstQuote+1:], '"')
if secondQuote == -1 {
return nil, errors.New("malformed json")
}
label := message[firstQuote+1 : firstQuote+1+secondQuote]
var v Envelope
switch {
case bytes.Contains(label, labelEvent):
switch label {
case "EVENT":
v = &EventEnvelope{}
case bytes.Contains(label, labelReq):
case "REQ":
v = &ReqEnvelope{}
case bytes.Contains(label, labelCount):
case "COUNT":
v = &CountEnvelope{}
case bytes.Contains(label, labelNotice):
case "NOTICE":
x := NoticeEnvelope("")
v = &x
case bytes.Contains(label, labelEose):
case "EOSE":
x := EOSEEnvelope("")
v = &x
case bytes.Contains(label, labelOk):
case "OK":
v = &OKEnvelope{}
case bytes.Contains(label, labelAuth):
case "AUTH":
v = &AuthEnvelope{}
case bytes.Contains(label, labelClosed):
case "CLOSED":
v = &ClosedEnvelope{}
case bytes.Contains(label, labelClose):
case "CLOSE":
x := CloseEnvelope("")
v = &x
default:
return nil, UnknownLabel
}
if err := v.UnmarshalJSON(message); err != nil {
if err := v.FromJSON(message); err != nil {
return nil, err
}
return v, nil

View File

@ -551,11 +551,11 @@ func (smp *sonicMessageParser) doneWithIntSlice(slice []int) {
// When an unexpected message (like ["NEG-OPEN", ...]) is found, the error UnknownLabel will be
// returned. Other errors will be returned if the JSON is malformed or the objects are not exactly
// as they should.
func (smp sonicMessageParser) ParseMessage(message []byte) (Envelope, error) {
func (smp sonicMessageParser) ParseMessage(message string) (Envelope, error) {
sv := &sonicVisitor{smp: &smp}
sv.whereWeAre = inEnvelope
err := ast.Preorder(unsafe.String(unsafe.SliceData(message), len(message)), sv, nil)
err := ast.Preorder(message, sv, nil)
return sv.mainEnvelope, err
}

View File

@ -1,3 +1,5 @@
//go:build sonic
package nostr
import (
@ -11,97 +13,97 @@ import (
func TestParseMessage(t *testing.T) {
testCases := []struct {
Name string
Message []byte
Message string
ExpectedEnvelope Envelope
}{
{
Name: "nil",
Message: nil,
Message: "",
ExpectedEnvelope: nil,
},
{
Name: "invalid string",
Message: []byte("invalid input"),
Message: "invalid input",
ExpectedEnvelope: nil,
},
{
Name: "invalid string with a comma",
Message: []byte("invalid, input"),
Message: "invalid, input",
ExpectedEnvelope: nil,
},
{
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"}]`),
Message: `["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"}]`),
Message: `["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"],["e","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"}]`),
Message: `["EVENT",{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["e","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{"e", "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"]`),
Message: `["NOTICE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`,
ExpectedEnvelope: ptr(NoticeEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")),
},
{
Name: "EOSE envelope",
Message: []byte(`["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`),
Message: `["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`,
ExpectedEnvelope: ptr(EOSEEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")),
},
{
Name: "COUNT envelope",
Message: []byte(`["COUNT","z",{"count":12}]`),
Message: `["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"}]`),
Message: `["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,""]`),
Message: `["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"]`),
Message: `["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"]`),
Message: `["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"]`),
Message: `["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"]`),
Message: `["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"}]`),
Message: `["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"]}]`),
Message: `["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"}}}}},
},
{
Name: "CLOSE envelope",
Message: []byte(`["CLOSE","subscription123"]`),
Message: `["CLOSE","subscription123"]`,
ExpectedEnvelope: ptr(CloseEnvelope("subscription123")),
},
}
@ -168,8 +170,8 @@ func TestParseMessagesFromFile(t *testing.T) {
continue
}
standardEnvelope := ParseMessage(line)
sonicEnvelope, err := smp.ParseMessage(line)
standardEnvelope := ParseMessage(string(line))
sonicEnvelope, err := smp.ParseMessage(string(line))
if standardEnvelope == nil {
require.Nil(t, sonicEnvelope, "line %d: standard parser returned nil but sonic parser didn't", lineNum)

View File

@ -1,7 +1,6 @@
package nostr
import (
"bytes"
"strconv"
"strings"
"sync"
@ -119,35 +118,39 @@ func isLowerHex(thing string) bool {
return true
}
func extractSubID(jsonStr []byte) string {
func extractSubID(jsonStr string) string {
// look for "EVENT" pattern
start := bytes.Index(jsonStr, []byte(`"EVENT"`))
start := strings.Index(jsonStr, `"EVENT"`)
if start == -1 {
return ""
}
// move to the next quote
offset := bytes.Index(jsonStr[start+7:], []byte{'"'})
offset := strings.Index(jsonStr[start+7:], `"`)
if offset == -1 {
return ""
}
start += 7 + offset + 1
// find the ending quote
end := bytes.Index(jsonStr[start:], []byte{'"'})
end := strings.Index(jsonStr[start:], `"`)
// get the contents
return unsafe.String(unsafe.SliceData(jsonStr[start:start+end]), end)
return jsonStr[start : start+end]
}
func extractEventID(jsonStr []byte) string {
// look for "id": pattern
start := bytes.Index(jsonStr, []byte(`"id":`))
func extractEventID(jsonStr string) string {
// look for "id" pattern
start := strings.Index(jsonStr, `"id"`)
if start == -1 {
return ""
}
// move to the next quote
offset := bytes.Index(jsonStr[start+4:], []byte{'"'})
offset := strings.IndexRune(jsonStr[start+4:], '"')
start += 4 + offset + 1
// get 64 characters of the id
return unsafe.String(unsafe.SliceData(jsonStr[start:start+64]), 64)
return jsonStr[start : start+64]
}

View File

@ -27,18 +27,18 @@ func TestIsLower(t *testing.T) {
func TestIDExtract(t *testing.T) {
{
data := []byte(`{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}`)
data := `{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}`
require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data))
}
{
data := []byte(`{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" }`)
data := `{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" }`
require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data))
}
}
func TestSubIdExtract(t *testing.T) {
{
data := []byte(`["EVENT", "xxz" ,{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]`)
data := `["EVENT", "xxz" ,{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]`
require.Equal(t, "xxz", extractSubID(data))
}
}

View File

@ -3,6 +3,8 @@ package nip77
import (
"bytes"
"fmt"
"strings"
"unsafe"
"github.com/mailru/easyjson"
jwriter "github.com/mailru/easyjson/jwriter"
@ -10,28 +12,28 @@ import (
"github.com/tidwall/gjson"
)
func ParseNegMessage(message []byte) nostr.Envelope {
firstComma := bytes.Index(message, []byte{','})
func ParseNegMessage(message string) nostr.Envelope {
firstComma := strings.Index(message, ",")
if firstComma == -1 {
return nil
}
label := message[0:firstComma]
var v nostr.Envelope
switch {
case bytes.Contains(label, []byte("NEG-MSG")):
switch label {
case "NEG-MSG":
v = &MessageEnvelope{}
case bytes.Contains(label, []byte("NEG-OPEN")):
case "NEG-OPEN":
v = &OpenEnvelope{}
case bytes.Contains(label, []byte("NEG-ERR")):
case "NEG-ERR":
v = &ErrorEnvelope{}
case bytes.Contains(label, []byte("NEG-CLOSE")):
case "NEG-CLOSE":
v = &CloseEnvelope{}
default:
return nil
}
if err := v.UnmarshalJSON(message); err != nil {
if err := v.FromJSON(message); err != nil {
return nil
}
return v
@ -56,8 +58,8 @@ func (v OpenEnvelope) String() string {
return string(b)
}
func (v *OpenEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *OpenEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) != 4 {
return fmt.Errorf("failed to decode NEG-OPEN envelope")
@ -65,7 +67,7 @@ func (v *OpenEnvelope) UnmarshalJSON(data []byte) error {
v.SubscriptionID = arr[1].Str
v.Message = arr[3].Str
return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Filter)
return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &v.Filter)
}
func (v OpenEnvelope) MarshalJSON() ([]byte, error) {
@ -97,8 +99,8 @@ func (v MessageEnvelope) String() string {
return string(b)
}
func (v *MessageEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *MessageEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 3 {
return fmt.Errorf("failed to decode NEG-MSG envelope")
@ -130,8 +132,8 @@ func (v CloseEnvelope) String() string {
return string(b)
}
func (v *CloseEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *CloseEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 2 {
return fmt.Errorf("failed to decode NEG-CLOSE envelope")
@ -159,8 +161,8 @@ func (v ErrorEnvelope) String() string {
return string(b)
}
func (v *ErrorEnvelope) UnmarshalJSON(data []byte) error {
r := gjson.ParseBytes(data)
func (v *ErrorEnvelope) FromJSON(data string) error {
r := gjson.Parse(data)
arr := r.Array()
if len(arr) < 3 {
return fmt.Errorf("failed to decode NEG-ERROR envelope")

View File

@ -20,7 +20,7 @@ func FetchIDsOnly(
result := make(chan error)
var r *nostr.Relay
r, err := nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data []byte) {
r, err := nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data string) {
envelope := ParseNegMessage(data)
if envelope == nil {
return

View File

@ -49,7 +49,7 @@ func NegentropySync(
result := make(chan error)
var r *nostr.Relay
r, err = nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data []byte) {
r, err = nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data string) {
envelope := ParseNegMessage(data)
if envelope == nil {
return

View File

@ -13,6 +13,7 @@ import (
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/puzpuzpuz/xsync/v3"
)
@ -35,7 +36,7 @@ type Relay struct {
challenge string // NIP-42 challenge, we only keep the last
noticeHandler func(string) // NIP-01 NOTICEs
customHandler func([]byte) // nonstandard unparseable messages
customHandler func(string) // nonstandard unparseable messages
okCallbacks *xsync.MapOf[string, func(bool, string)]
writeQueue chan writeRequest
subscriptionChannelCloseQueue chan *Subscription
@ -104,7 +105,7 @@ func (nh WithNoticeHandler) ApplyRelayOption(r *Relay) {
// WithCustomHandler must be a function that handles any relay message that couldn't be
// parsed as a standard envelope.
type WithCustomHandler func(data []byte)
type WithCustomHandler func(data string)
func (ch WithCustomHandler) ApplyRelayOption(r *Relay) {
r.customHandler = ch
@ -212,6 +213,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
// general message reader loop
go func() {
buf := new(bytes.Buffer)
mp := NewMessageParser()
for {
buf.Reset()
@ -222,7 +224,8 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
break
}
message := buf.Bytes()
msgb := buf.Bytes()
message := unsafe.String(unsafe.SliceData(msgb), len(msgb))
debugLogf("{%s} received %v\n", r.URL, message)
// if this is an "EVENT" we will have this preparser logic that should speed things up a little
@ -235,9 +238,9 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
}
}
envelope := ParseMessage(message)
envelope, err := mp.ParseMessage(message)
if envelope == nil {
if r.customHandler != nil {
if r.customHandler != nil && err == UnknownLabel {
r.customHandler(message)
}
continue
@ -258,13 +261,13 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
r.challenge = *env.Challenge
case *EventEnvelope:
// we already have the subscription from the pre-check above, so we can just reuse it
if subscription == nil {
if sub == nil {
// InfoLogger.Printf("{%s} no subscription with id '%s'\n", r.URL, *env.SubscriptionID)
continue
} else {
// check if the event matches the desired filter, ignore otherwise
if !subscription.match(&env.Event) {
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, subscription.Filters, env.Event)
if !sub.match(&env.Event) {
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filters, env.Event)
continue
}
@ -277,7 +280,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
}
// dispatch this to the internal .events channel of the subscription
subscription.dispatchEvent(&env.Event)
sub.dispatchEvent(&env.Event)
}
case *EOSEEnvelope:
if subscription, ok := r.Subscriptions.Load(subIdToSerial(string(*env))); ok {