+14
-10
api/server/feedgetlikes.go
+14
-10
api/server/feedgetlikes.go
···
8
"github.com/labstack/echo/v4"
9
vyletdatabase "github.com/vylet-app/go/database/proto"
10
"github.com/vylet-app/go/generated/vylet"
11
)
12
13
type GetFeedLikesBySubjectInput struct {
14
Uri string `query:"uri"`
15
-
Limit int64 `query:"limit"`
16
Cursor *string `query:"cursor"`
17
}
18
19
-
func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, error) {
20
logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri)
21
22
resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{
···
25
Cursor: cursor,
26
})
27
if err != nil {
28
-
return nil, fmt.Errorf("failed to get likes by subject: %w", err)
29
}
30
31
dids := make([]string, 0, len(resp.Likes))
···
35
36
profiles, err := s.getProfiles(ctx, dids)
37
if err != nil {
38
-
return nil, fmt.Errorf("failed to get profiles for subject: %w", err)
39
}
40
41
likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes))
···
53
})
54
}
55
56
-
return likes, nil
57
}
58
59
-
func (s *Server) handleGetLikesBySubject(e echo.Context) error {
60
ctx := e.Request().Context()
61
62
-
logger := s.logger.With("name", "handleGetLikesByPost")
63
64
var input GetFeedLikesBySubjectInput
65
if err := e.Bind(&input); err != nil {
···
71
return NewValidationError("uri", "URI must be provided")
72
}
73
74
-
if input.Limit < 1 || input.Limit > 100 {
75
return NewValidationError("limit", "limit must be between 1 and 100")
76
}
77
78
logger = logger.With("uri", input.Uri)
79
80
-
likes, err := s.getLikesBySubject(ctx, input.Uri, input.Limit, input.Cursor)
81
if err != nil {
82
logger.Error("failed to get subject likes", "err", err)
83
return ErrInternalServerErr
84
}
85
86
return e.JSON(200, vylet.FeedGetSubjectLikes_Output{
87
-
Likes: likes,
88
})
89
}
···
8
"github.com/labstack/echo/v4"
9
vyletdatabase "github.com/vylet-app/go/database/proto"
10
"github.com/vylet-app/go/generated/vylet"
11
+
"github.com/vylet-app/go/internal/helpers"
12
)
13
14
type GetFeedLikesBySubjectInput struct {
15
Uri string `query:"uri"`
16
+
Limit *int64 `query:"limit"`
17
Cursor *string `query:"cursor"`
18
}
19
20
+
func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, *string, error) {
21
logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri)
22
23
resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{
···
26
Cursor: cursor,
27
})
28
if err != nil {
29
+
return nil, nil, fmt.Errorf("failed to get likes by subject: %w", err)
30
}
31
32
dids := make([]string, 0, len(resp.Likes))
···
36
37
profiles, err := s.getProfiles(ctx, dids)
38
if err != nil {
39
+
return nil, nil, fmt.Errorf("failed to get profiles for subject: %w", err)
40
}
41
42
likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes))
···
54
})
55
}
56
57
+
return likes, resp.Cursor, nil
58
}
59
60
+
func (s *Server) handleGetSubjectLikes(e echo.Context) error {
61
ctx := e.Request().Context()
62
63
+
logger := s.logger.With("name", "handleGetSubjectLikes")
64
65
var input GetFeedLikesBySubjectInput
66
if err := e.Bind(&input); err != nil {
···
72
return NewValidationError("uri", "URI must be provided")
73
}
74
75
+
if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) {
76
return NewValidationError("limit", "limit must be between 1 and 100")
77
+
} else if input.Limit == nil {
78
+
input.Limit = helpers.ToInt64Ptr(25)
79
}
80
81
logger = logger.With("uri", input.Uri)
82
83
+
likes, cursor, err := s.getLikesBySubject(ctx, input.Uri, *input.Limit, input.Cursor)
84
if err != nil {
85
logger.Error("failed to get subject likes", "err", err)
86
return ErrInternalServerErr
87
}
88
89
return e.JSON(200, vylet.FeedGetSubjectLikes_Output{
90
+
Likes: likes,
91
+
Cursor: cursor,
92
})
93
}
+87
-9
api/server/feedgetposts.go
+87
-9
api/server/feedgetposts.go
···
3
import (
4
"context"
5
"encoding/json"
6
"fmt"
7
"time"
8
9
"github.com/bluesky-social/indigo/lex/util"
···
14
"github.com/vylet-app/go/internal/helpers"
15
"golang.org/x/sync/errgroup"
16
)
17
-
18
-
type GetFeedPostsInput struct {
19
-
Uris []string `query:"uris"`
20
-
}
21
22
func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) {
23
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
···
82
}
83
84
func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) {
85
-
logger := s.logger.With("name", "feedPostsToPostViews")
86
-
87
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
88
Uris: uris,
89
})
···
94
return nil, ErrDatabaseNotFound
95
}
96
97
-
dids := make([]string, 0, len(resp.Posts))
98
addedDids := make(map[string]struct{})
99
-
for _, post := range resp.Posts {
100
if _, ok := addedDids[post.AuthorDid]; ok {
101
continue
102
}
···
131
}
132
133
feedPostViews := make(map[string]*vylet.FeedDefs_PostView)
134
-
for _, post := range resp.Posts {
135
profileBasic, ok := profiles[post.AuthorDid]
136
if !ok {
137
logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri)
···
197
return feedPostViews, nil
198
}
199
200
func (s *Server) handleGetPosts(e echo.Context) error {
201
ctx := e.Request().Context()
202
···
245
Posts: orderedPostViews,
246
})
247
}
···
3
import (
4
"context"
5
"encoding/json"
6
+
"errors"
7
"fmt"
8
+
"sort"
9
"time"
10
11
"github.com/bluesky-social/indigo/lex/util"
···
16
"github.com/vylet-app/go/internal/helpers"
17
"golang.org/x/sync/errgroup"
18
)
19
20
func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) {
21
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
···
80
}
81
82
func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) {
83
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
84
Uris: uris,
85
})
···
90
return nil, ErrDatabaseNotFound
91
}
92
93
+
feedPostViews, err := s.postsToPostViews(ctx, resp.Posts, viewer)
94
+
if err != nil {
95
+
return nil, err
96
+
}
97
+
98
+
return feedPostViews, nil
99
+
}
100
+
101
+
func (s *Server) postsToPostViews(ctx context.Context, posts map[string]*vyletdatabase.Post, viewer string) (map[string]*vylet.FeedDefs_PostView, error) {
102
+
logger := s.logger.With("name", "postsToPostViews")
103
+
104
+
uris := make([]string, 0, len(posts))
105
+
dids := make([]string, 0, len(posts))
106
addedDids := make(map[string]struct{})
107
+
for uri, post := range posts {
108
+
uris = append(uris, uri)
109
+
110
if _, ok := addedDids[post.AuthorDid]; ok {
111
continue
112
}
···
141
}
142
143
feedPostViews := make(map[string]*vylet.FeedDefs_PostView)
144
+
for _, post := range posts {
145
profileBasic, ok := profiles[post.AuthorDid]
146
if !ok {
147
logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri)
···
207
return feedPostViews, nil
208
}
209
210
+
type GetFeedPostsInput struct {
211
+
Uris []string `query:"uris"`
212
+
}
213
+
214
func (s *Server) handleGetPosts(e echo.Context) error {
215
ctx := e.Request().Context()
216
···
259
Posts: orderedPostViews,
260
})
261
}
262
+
263
+
type GetFeedActorPostsInput struct {
264
+
Actor string `query:"actor"`
265
+
Limit *int64 `query:"limit"`
266
+
Cursor *string `query:"cursor"`
267
+
}
268
+
269
+
func (s *Server) handleGetActorPosts(e echo.Context) error {
270
+
ctx := e.Request().Context()
271
+
272
+
logger := s.logger.With("name", "handleGetActorPosts")
273
+
274
+
var input GetFeedActorPostsInput
275
+
if err := e.Bind(&input); err != nil {
276
+
logger.Error("failed to bind", "err", err)
277
+
return ErrInternalServerErr
278
+
}
279
+
280
+
if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) {
281
+
return NewValidationError("limit", "limit must be between 1 and 100")
282
+
} else if input.Limit == nil {
283
+
input.Limit = helpers.ToInt64Ptr(25)
284
+
}
285
+
286
+
logger = logger.With("actor", input.Actor, "limit", *input.Limit, "cursor", input.Cursor)
287
+
288
+
did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor)
289
+
if err != nil {
290
+
if errors.Is(err, ErrActorNotValid) {
291
+
return NewValidationError("actor", "actor must be a valid DID or handle")
292
+
}
293
+
logger.Error("error fetching did and handle", "err", err)
294
+
return ErrInternalServerErr
295
+
}
296
+
297
+
resp, err := s.client.Post.GetPostsByActor(ctx, &vyletdatabase.GetPostsByActorRequest{
298
+
Did: did,
299
+
Limit: *input.Limit,
300
+
Cursor: input.Cursor,
301
+
})
302
+
if err != nil {
303
+
logger.Error("failed to get posts", "did", did)
304
+
return ErrInternalServerErr
305
+
}
306
+
307
+
postViews, err := s.postsToPostViews(ctx, resp.Posts, "") // TODO: set viewer
308
+
if err != nil {
309
+
s.logger.Error("failed to get post views", "err", err)
310
+
return ErrInternalServerErr
311
+
}
312
+
313
+
sortedPostViews := make([]*vylet.FeedDefs_PostView, 0, len(postViews))
314
+
for _, postView := range postViews {
315
+
sortedPostViews = append(sortedPostViews, postView)
316
+
}
317
+
sort.Slice(sortedPostViews, func(i, j int) bool {
318
+
return sortedPostViews[i].CreatedAt > sortedPostViews[j].CreatedAt
319
+
})
320
+
321
+
return e.JSON(200, vylet.FeedGetActorPosts_Output{
322
+
Posts: sortedPostViews,
323
+
Cursor: resp.Cursor,
324
+
})
325
+
}
+2
-1
api/server/server.go
+2
-1
api/server/server.go
···
129
130
// app.vylet.feed
131
s.echo.GET("/xrpc/app.vylet.feed.getPosts", s.handleGetPosts)
132
+
s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetSubjectLikes)
133
+
s.echo.GET("/xrpc/app.vylet.feed.getActorPosts", s.handleGetActorPosts)
134
}
135
136
func (s *Server) errorHandler(err error, c echo.Context) {
+76
bench.py
+76
bench.py
···
···
1
+
import requests
2
+
import time
3
+
import sys
4
+
from datetime import datetime
5
+
6
+
# Configuration
7
+
URL = "http://localhost:8080/xrpc/app.vylet.feed.getActorPosts?actor=hailey.at"
8
+
INITIAL_DELAY = 5.0
9
+
MIN_DELAY = 0.001
10
+
RAMP_FACTOR = 0.5
11
+
12
+
13
+
def make_request(session, request_num, delay):
14
+
try:
15
+
start_time = time.time()
16
+
response = session.get(URL, timeout=10)
17
+
elapsed = time.time() - start_time
18
+
19
+
timestamp = datetime.now().strftime("%H:%M:%S")
20
+
rate = 1 / delay if delay > 0 else float("inf")
21
+
22
+
print(
23
+
f"[{timestamp}] Request #{request_num:4d} | "
24
+
f"Status: {response.status_code} | "
25
+
f"Time: {elapsed:.3f}s | "
26
+
f"Rate: {rate:.2f} req/s"
27
+
)
28
+
29
+
return True
30
+
except requests.exceptions.RequestException as e:
31
+
timestamp = datetime.now().strftime("%H:%M:%S")
32
+
print(f"[{timestamp}] Request #{request_num:4d} | ERROR: {e}")
33
+
return False
34
+
35
+
36
+
def main():
37
+
print("=" * 70)
38
+
print("Gradual Load Tester")
39
+
print("=" * 70)
40
+
print(f"Target URL: {URL}")
41
+
print(f"Starting delay: {INITIAL_DELAY}s ({1 / INITIAL_DELAY:.2f} req/s)")
42
+
print(f"Min delay: {MIN_DELAY}s ({1 / MIN_DELAY:.2f} req/s)")
43
+
print(
44
+
f"Ramp factor: {RAMP_FACTOR} (gets {(1 - RAMP_FACTOR) * 100:.0f}% faster each cycle)"
45
+
)
46
+
print("=" * 70)
47
+
print("\nPress Ctrl+C to stop\n")
48
+
49
+
session = requests.Session()
50
+
delay = INITIAL_DELAY
51
+
request_num = 0
52
+
53
+
try:
54
+
while True:
55
+
request_num += 1
56
+
make_request(session, request_num, delay)
57
+
58
+
time.sleep(delay)
59
+
60
+
if delay > MIN_DELAY:
61
+
delay = max(delay * RAMP_FACTOR, MIN_DELAY)
62
+
if delay == MIN_DELAY:
63
+
print(f"\n{'=' * 70}")
64
+
print(f"Reached maximum rate: {1 / MIN_DELAY:.2f} requests/second")
65
+
print(f"{'=' * 70}\n")
66
+
67
+
except KeyboardInterrupt:
68
+
print(f"\n\n{'=' * 70}")
69
+
print(f"Stopped after {request_num} requests")
70
+
print(f"Final rate: {1 / delay:.2f} requests/second")
71
+
print(f"{'=' * 70}")
72
+
sys.exit(0)
73
+
74
+
75
+
if __name__ == "__main__":
76
+
main()
+36
generated/vylet/feedgetActorPosts.go
+36
generated/vylet/feedgetActorPosts.go
···
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
// Lexicon schema: app.vylet.feed.getActorPosts
4
+
5
+
package vylet
6
+
7
+
import (
8
+
"context"
9
+
10
+
lexutil "github.com/bluesky-social/indigo/lex/util"
11
+
)
12
+
13
+
// FeedGetActorPosts_Output is the output of a app.vylet.feed.getActorPosts call.
14
+
type FeedGetActorPosts_Output struct {
15
+
Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"`
16
+
Posts []*FeedDefs_PostView `json:"posts" cborgen:"posts"`
17
+
}
18
+
19
+
// FeedGetActorPosts calls the XRPC method "app.vylet.feed.getActorPosts".
20
+
func FeedGetActorPosts(ctx context.Context, c lexutil.LexClient, actor string, cursor string, limit int64) (*FeedGetActorPosts_Output, error) {
21
+
var out FeedGetActorPosts_Output
22
+
23
+
params := map[string]interface{}{}
24
+
params["actor"] = actor
25
+
if cursor != "" {
26
+
params["cursor"] = cursor
27
+
}
28
+
if limit != 0 {
29
+
params["limit"] = limit
30
+
}
31
+
if err := c.LexDo(ctx, lexutil.Query, "", "app.vylet.feed.getActorPosts", params, nil, &out); err != nil {
32
+
return nil, err
33
+
}
34
+
35
+
return &out, nil
36
+
}
+8
internal/helpers/helpers.go
+8
internal/helpers/helpers.go