+20
-14
knotclient/events.go
+20
-14
knotclient/events.go
···
7
7
"log/slog"
8
8
"math/rand"
9
9
"net/url"
10
+
"strconv"
10
11
"sync"
11
12
"time"
12
13
···
62
63
}
63
64
64
65
type CursorStore interface {
65
-
Set(knot, cursor string)
66
-
Get(knot string) (cursor string)
66
+
Set(knot string, cursor int64)
67
+
Get(knot string) (cursor int64)
67
68
}
68
69
69
70
type RedisCursorStore struct {
···
80
81
cursorKey = "cursor:%s"
81
82
)
82
83
83
-
func (r *RedisCursorStore) Set(knot, cursor string) {
84
+
func (r *RedisCursorStore) Set(knot string, cursor int64) {
84
85
key := fmt.Sprintf(cursorKey, knot)
85
86
r.rdb.Set(context.Background(), key, cursor, 0)
86
87
}
87
88
88
-
func (r *RedisCursorStore) Get(knot string) (cursor string) {
89
+
func (r *RedisCursorStore) Get(knot string) (cursor int64) {
89
90
key := fmt.Sprintf(cursorKey, knot)
90
91
val, err := r.rdb.Get(context.Background(), key).Result()
91
92
if err != nil {
92
-
return ""
93
+
return 0
93
94
}
94
95
95
-
return val
96
+
cursor, err = strconv.ParseInt(val, 10, 64)
97
+
if err != nil {
98
+
return 0 // optionally log parsing error
99
+
}
100
+
101
+
return cursor
96
102
}
97
103
98
104
type MemoryCursorStore struct {
99
105
store sync.Map
100
106
}
101
107
102
-
func (m *MemoryCursorStore) Set(knot, cursor string) {
108
+
func (m *MemoryCursorStore) Set(knot string, cursor int64) {
103
109
m.store.Store(knot, cursor)
104
110
}
105
111
106
-
func (m *MemoryCursorStore) Get(knot string) (cursor string) {
112
+
func (m *MemoryCursorStore) Get(knot string) (cursor int64) {
107
113
if result, ok := m.store.Load(knot); ok {
108
-
if val, ok := result.(string); ok {
114
+
if val, ok := result.(int64); ok {
109
115
return val
110
116
}
111
117
}
112
118
113
-
return ""
119
+
return 0
114
120
}
115
121
116
-
func (e *EventConsumer) buildUrl(s EventSource, cursor string) (*url.URL, error) {
122
+
func (e *EventConsumer) buildUrl(s EventSource, cursor int64) (*url.URL, error) {
117
123
scheme := "wss"
118
124
if e.cfg.Dev {
119
125
scheme = "ws"
···
124
130
return nil, err
125
131
}
126
132
127
-
if cursor != "" {
133
+
if cursor != 0 {
128
134
query := url.Values{}
129
-
query.Add("cursor", cursor)
135
+
query.Add("cursor", fmt.Sprintf("%d", cursor))
130
136
u.RawQuery = query.Encode()
131
137
}
132
138
return u, nil
···
222
228
}
223
229
224
230
// update cursor
225
-
c.cfg.CursorStore.Set(j.source.Knot, msg.Rkey)
231
+
c.cfg.CursorStore.Set(j.source.Knot, time.Now().Unix())
226
232
227
233
if err := c.cfg.ProcessFunc(j.source, msg); err != nil {
228
234
c.logger.Error("error processing message", "source", j.source, "err", err)
+10
-6
knotserver/db/events.go
+10
-6
knotserver/db/events.go
···
10
10
Rkey string `json:"rkey"`
11
11
Nsid string `json:"nsid"`
12
12
EventJson string `json:"event"`
13
+
Created int64 `json:"created"`
13
14
}
14
15
15
16
func (d *DB) InsertEvent(event Event, notifier *notifier.Notifier) error {
17
+
16
18
_, err := d.db.Exec(
17
19
`insert into events (rkey, nsid, event) values (?, ?, ?)`,
18
20
event.Rkey,
···
25
27
return err
26
28
}
27
29
28
-
func (d *DB) GetEvents(cursor string) ([]Event, error) {
30
+
func (d *DB) GetEvents(cursor int64) ([]Event, error) {
29
31
whereClause := ""
30
32
args := []any{}
31
-
if cursor != "" {
32
-
whereClause = "where rkey > ?"
33
+
if cursor > 0 {
34
+
whereClause = "where created > ?"
33
35
args = append(args, cursor)
34
36
}
35
37
36
38
query := fmt.Sprintf(`
37
-
select rkey, nsid, event
39
+
select rkey, nsid, event, created
38
40
from events
39
41
%s
40
-
order by rkey asc
42
+
order by created asc
41
43
limit 100
42
44
`, whereClause)
43
45
···
50
52
var evts []Event
51
53
for rows.Next() {
52
54
var ev Event
53
-
rows.Scan(&ev.Rkey, &ev.Nsid, &ev.EventJson)
55
+
if err := rows.Scan(&ev.Rkey, &ev.Nsid, &ev.EventJson, &ev.Created); err != nil {
56
+
return nil, err
57
+
}
54
58
evts = append(evts, ev)
55
59
}
56
60
+1
knotserver/db/init.go
+1
knotserver/db/init.go
+8
-3
knotserver/events.go
+8
-3
knotserver/events.go
···
4
4
"context"
5
5
"encoding/json"
6
6
"net/http"
7
+
"strconv"
7
8
"time"
8
9
9
10
"github.com/gorilla/websocket"
···
42
43
}
43
44
}()
44
45
45
-
cursor := r.URL.Query().Get("cursor")
46
+
cursorStr := r.URL.Query().Get("cursor")
47
+
cursor, err := strconv.ParseInt(cursorStr, 10, 64)
48
+
if err != nil {
49
+
l.Error("empty or invalid cursor, defaulting to zero", "invalidCursor", cursorStr)
50
+
}
46
51
47
52
// complete backfill first before going to live data
48
53
l.Debug("going through backfill", "cursor", cursor)
···
74
79
}
75
80
}
76
81
77
-
func (h *Handle) streamOps(conn *websocket.Conn, cursor *string) error {
82
+
func (h *Handle) streamOps(conn *websocket.Conn, cursor *int64) error {
78
83
events, err := h.db.GetEvents(*cursor)
79
84
if err != nil {
80
85
h.l.Error("failed to fetch events from db", "err", err, "cursor", cursor)
···
105
110
h.l.Debug("err", "err", err)
106
111
return err
107
112
}
108
-
*cursor = event.Rkey
113
+
*cursor = event.Created
109
114
}
110
115
111
116
return nil
+1
-1
nix/vm.nix
+1
-1
nix/vm.nix
···
21
21
g = config.services.tangled-knot.gitUser;
22
22
in [
23
23
"d /var/lib/knot 0770 ${u} ${g} - -" # Create the directory first
24
-
"f+ /var/lib/knot/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=40b4db20544e37a12ba3ed7353d4d4421a30e0593385068d2ef85263495794d8"
24
+
"f+ /var/lib/knot/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=16154910ef55fe48121082c0b51fc0e360a8b15eb7bda7991d88dc9f7684427a"
25
25
];
26
26
services.tangled-knot = {
27
27
enable = true;