forked from tangled.org/core
this repo has no description

knotserver: init jetstream

Changed files
+206 -5
cmd
knotserver
knotserver
+1 -1
cmd/knotserver/main.go
···
log.Fatalf("failed to setup db: %s", err)
}
-
mux, err := knotserver.Setup(c, db)
+
mux, err := knotserver.Setup(ctx, c, db)
if err != nil {
log.Fatal(err)
}
+1
go.mod
···
github.com/go-git/go-git/v5 v5.12.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0
+
github.com/gorilla/websocket v1.5.1
github.com/ipfs/go-cid v0.4.1
github.com/mattn/go-sqlite3 v1.14.24
github.com/microcosm-cc/bluemonday v1.0.27
+2
go.sum
···
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+41 -4
knotserver/handler.go
···
package knotserver
import (
+
"context"
+
"encoding/json"
"fmt"
+
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/icyphox/bild/knotserver/config"
"github.com/icyphox/bild/knotserver/db"
+
"github.com/icyphox/bild/knotserver/jsclient"
)
-
func Setup(c *config.Config, db *db.DB) (http.Handler, error) {
+
type Handle struct {
+
c *config.Config
+
db *db.DB
+
js *jsclient.JetstreamClient
+
}
+
+
func Setup(ctx context.Context, c *config.Config, db *db.DB) (http.Handler, error) {
r := chi.NewRouter()
h := Handle{
c: c,
db: db,
+
}
+
+
err := h.StartJetstream(ctx)
+
if err != nil {
+
return nil, fmt.Errorf("failed to start jetstream: %w", err)
}
r.Get("/", h.Index)
···
return r, nil
}
-
type Handle struct {
-
c *config.Config
-
db *db.DB
+
func (h *Handle) StartJetstream(ctx context.Context) error {
+
colections := []string{"sh.bild.publicKeys"}
+
dids := []string{}
+
+
h.js = jsclient.NewJetstreamClient(colections, dids)
+
messages, err := h.js.ReadJetstream(ctx)
+
if err != nil {
+
return fmt.Errorf("failed to read from jetstream: %w", err)
+
}
+
+
go func() {
+
for msg := range messages {
+
var data map[string]interface{}
+
if err := json.Unmarshal(msg, &data); err != nil {
+
log.Printf("error unmarshaling message: %v", err)
+
continue
+
}
+
+
if kind, ok := data["kind"].(string); ok && kind == "commit" {
+
log.Printf("commit event: %+v", data)
+
}
+
}
+
}()
+
+
return nil
}
func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) {
+161
knotserver/jsclient/jetstream.go
···
+
package jsclient
+
+
import (
+
"context"
+
"fmt"
+
"log"
+
"net/url"
+
"sync"
+
"time"
+
+
"github.com/gorilla/websocket"
+
)
+
+
type JetstreamClient struct {
+
collections []string
+
dids []string
+
conn *websocket.Conn
+
mu sync.RWMutex
+
reconnectCh chan struct{}
+
}
+
+
func NewJetstreamClient(collections, dids []string) *JetstreamClient {
+
return &JetstreamClient{
+
collections: collections,
+
dids: dids,
+
reconnectCh: make(chan struct{}, 1),
+
}
+
}
+
+
func (j *JetstreamClient) buildWebsocketURL(queryParams string) url.URL {
+
+
u := url.URL{
+
Scheme: "wss",
+
Host: "jetstream1.us-west.bsky.network",
+
Path: "/subscribe",
+
RawQuery: queryParams,
+
}
+
+
fmt.Println("URL:", u.String())
+
return u
+
}
+
+
// UpdateCollections updates the collections list and triggers a reconnection
+
func (j *JetstreamClient) UpdateCollections(collections []string) {
+
j.mu.Lock()
+
j.collections = collections
+
j.mu.Unlock()
+
j.triggerReconnect()
+
}
+
+
// UpdateDids updates the DIDs list and triggers a reconnection
+
func (j *JetstreamClient) UpdateDids(dids []string) {
+
j.mu.Lock()
+
j.dids = dids
+
j.mu.Unlock()
+
j.triggerReconnect()
+
}
+
+
func (j *JetstreamClient) triggerReconnect() {
+
select {
+
case j.reconnectCh <- struct{}{}:
+
default:
+
// Channel already has a pending reconnect
+
}
+
}
+
+
func (j *JetstreamClient) buildQueryParams(cursor int64) string {
+
j.mu.RLock()
+
defer j.mu.RUnlock()
+
+
var collections, dids string
+
if len(j.collections) > 0 {
+
collections = fmt.Sprintf("wantedCollections=%s", j.collections[0])
+
for _, collection := range j.collections[1:] {
+
collections += fmt.Sprintf("&wantedCollections=%s", collection)
+
}
+
}
+
if len(j.dids) > 0 {
+
for i, did := range j.dids {
+
if i == 0 {
+
dids = fmt.Sprintf("wantedDids=%s", did)
+
} else {
+
dids += fmt.Sprintf("&wantedDids=%s", did)
+
}
+
}
+
}
+
+
var queryStr string
+
if collections != "" && dids != "" {
+
queryStr = collections + "&" + dids
+
} else if collections != "" {
+
queryStr = collections
+
} else if dids != "" {
+
queryStr = dids
+
}
+
+
return queryStr
+
}
+
+
func (j *JetstreamClient) connect(cursor int64) error {
+
queryParams := j.buildQueryParams(cursor)
+
u := j.buildWebsocketURL(queryParams)
+
+
dialer := websocket.Dialer{
+
HandshakeTimeout: 10 * time.Second,
+
}
+
+
conn, _, err := dialer.Dial(u.String(), nil)
+
if err != nil {
+
return err
+
}
+
+
if j.conn != nil {
+
j.conn.Close()
+
}
+
j.conn = conn
+
return nil
+
}
+
+
func (j *JetstreamClient) readMessages(ctx context.Context, messages chan []byte) {
+
defer close(messages)
+
defer j.conn.Close()
+
+
ticker := time.NewTicker(1 * time.Second)
+
defer ticker.Stop()
+
+
for {
+
select {
+
case <-ctx.Done():
+
return
+
case <-j.reconnectCh:
+
// Reconnect with new parameters
+
// cursor := time.Now().Add(-5 * time.Second).UnixMicro()
+
if err := j.connect(0); err != nil {
+
log.Printf("error reconnecting to jetstream: %v", err)
+
return
+
}
+
case <-ticker.C:
+
_, message, err := j.conn.ReadMessage()
+
if err != nil {
+
log.Printf("error reading from websocket: %v", err)
+
return
+
}
+
messages <- message
+
}
+
}
+
}
+
+
func (j *JetstreamClient) ReadJetstream(ctx context.Context) (chan []byte, error) {
+
fiveSecondsAgo := time.Now().Add(-5 * time.Second).UnixMicro()
+
+
if err := j.connect(fiveSecondsAgo); err != nil {
+
log.Printf("error connecting to jetstream: %v", err)
+
return nil, err
+
}
+
+
messages := make(chan []byte)
+
go j.readMessages(ctx, messages)
+
+
return messages, nil
+
}