add internal/{processor,streaming}
This commit is contained in:
		
							parent
							
								
									6f17b59a47
								
							
						
					
					
						commit
						5c6da51234
					
				
					 6 changed files with 128 additions and 29 deletions
				
			
		|  | @ -92,6 +92,6 @@ func (app *App) Create(w http.ResponseWriter, r *http.Request) (api.Post, error) | |||
| 		return api.Post{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: federate post + push to websockets | ||||
| 	go app.Processor.HandlePost(post) | ||||
| 	return api.DBPostToPost(post, blog, acct), nil | ||||
| } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import ( | |||
| 	"errors" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.sleepycat.moe/sam/mercury/internal/streaming" | ||||
| 	"git.sleepycat.moe/sam/mercury/web/api" | ||||
| 	"git.sleepycat.moe/sam/mercury/web/app" | ||||
| 	"github.com/gorilla/websocket" | ||||
|  | @ -38,7 +39,7 @@ func (app *App) Streaming(w http.ResponseWriter, r *http.Request) error { | |||
| 	} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	socket, ok := SocketHolder.socketsFor(token.UserID).newSocket(ctx, cancel) | ||||
| 	socket, ok := app.Processor.SocketHolder.SocketsFor(token.UserID).NewSocket(ctx, cancel) | ||||
| 	if !ok { | ||||
| 		err := conn.WriteJSON(newEvent(EventTypeError, ErrorEvent{Code: api.ErrTooManyStreams, Message: "Too many streams open"})) | ||||
| 		if err != nil { | ||||
|  | @ -54,43 +55,43 @@ func (app *App) Streaming(w http.ResponseWriter, r *http.Request) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (app *App) writeStream(conn *websocket.Conn, socket *socket) { | ||||
| func (app *App) writeStream(conn *websocket.Conn, socket *streaming.Socket) { | ||||
| 	defer conn.Close() | ||||
| 
 | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-socket.ctx.Done(): | ||||
| 		case <-socket.Done(): | ||||
| 			return | ||||
| 		case ev := <-socket.ch: | ||||
| 		case ev := <-socket.Chan(): | ||||
| 			// at this point, the type should already have been filtered, so just send the event | ||||
| 			err := conn.WriteJSON(ev) | ||||
| 			if err != nil { | ||||
| 				// write failed, bail and make client reconnect | ||||
| 				log.Err(err).Msg("error writing JSON to socket") | ||||
| 				socket.cancel() | ||||
| 				socket.Cancel() | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (app *App) readStream(conn *websocket.Conn, socket *socket) { | ||||
| func (app *App) readStream(conn *websocket.Conn, socket *streaming.Socket) { | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-socket.ctx.Done(): | ||||
| 		case <-socket.Done(): | ||||
| 			return | ||||
| 		default: | ||||
| 			var e IncomingEvent | ||||
| 			var e streaming.IncomingEvent | ||||
| 			err := conn.ReadJSON(&e) | ||||
| 			if err != nil { | ||||
| 				// read failed, bail and make client reconnect | ||||
| 				log.Err(err).Msg("error reading JSON from socket") | ||||
| 				socket.cancel() | ||||
| 				socket.Cancel() | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			switch e.Type { | ||||
| 			case EventTypeSubscribe, EventTypeUnsubscribe: | ||||
| 				var et EventType | ||||
| 			case streaming.EventTypeSubscribe, streaming.EventTypeUnsubscribe: | ||||
| 				var et streaming.EventType | ||||
| 				err = json.Unmarshal(e.Data, &et) | ||||
| 				if err != nil { | ||||
| 					// invalid event type, log but don't disconnect | ||||
|  | @ -103,7 +104,7 @@ func (app *App) readStream(conn *websocket.Conn, socket *socket) { | |||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				socket.setEvent(et, e.Type != EventTypeSubscribe) | ||||
| 				socket.SetEvent(et, e.Type != streaming.EventTypeUnsubscribe) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,103 +0,0 @@ | |||
| package streaming | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/oklog/ulid/v2" | ||||
| ) | ||||
| 
 | ||||
| var SocketHolder socketHolder | ||||
| 
 | ||||
| type socketHolder struct { | ||||
| 	// map of sockets to | ||||
| 	sockets map[ulid.ULID]*userSockets | ||||
| 	mu      sync.Mutex | ||||
| } | ||||
| 
 | ||||
| func (sh *socketHolder) Send(acctID ulid.ULID, et EventType, data any) { | ||||
| 	userSockets := sh.socketsFor(acctID) | ||||
| 
 | ||||
| 	userSockets.mu.Lock() | ||||
| 	sockets := make([]*socket, len(userSockets.sockets)) | ||||
| 	copy(sockets, userSockets.sockets) | ||||
| 	userSockets.mu.Unlock() | ||||
| 
 | ||||
| 	for _, s := range sockets { | ||||
| 		if s.willAcceptEvent(et) { | ||||
| 			// the socket might block for a bit, so spin this off into a separate goroutine | ||||
| 			go func(s *socket) { | ||||
| 				s.ch <- Event{Type: et, Data: data} | ||||
| 			}(s) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *socketHolder) socketsFor(acct ulid.ULID) *userSockets { | ||||
| 	s.mu.Lock() | ||||
| 	defer s.mu.Unlock() | ||||
| 
 | ||||
| 	us, ok := s.sockets[acct] | ||||
| 	if !ok { | ||||
| 		us = &userSockets{} | ||||
| 		s.sockets[acct] = us | ||||
| 	} | ||||
| 	return us | ||||
| } | ||||
| 
 | ||||
| const sessionCountLimit = 50 // no more than 50 concurrent sessions per user | ||||
| 
 | ||||
| type userSockets struct { | ||||
| 	mu      sync.Mutex | ||||
| 	sockets []*socket | ||||
| } | ||||
| 
 | ||||
| func (s *userSockets) newSocket(ctx context.Context, cancel context.CancelFunc) (*socket, bool) { | ||||
| 	s.mu.Lock() | ||||
| 	if len(s.sockets) >= sessionCountLimit { | ||||
| 		return nil, false | ||||
| 	} | ||||
| 	socket := newSocket(ctx, cancel) | ||||
| 	s.sockets = append(s.sockets, socket) | ||||
| 	return socket, true | ||||
| } | ||||
| 
 | ||||
| type socket struct { | ||||
| 	ctx    context.Context | ||||
| 	cancel context.CancelFunc | ||||
| 
 | ||||
| 	ch    chan Event | ||||
| 	types map[EventType]struct{} | ||||
| 
 | ||||
| 	mu sync.RWMutex | ||||
| } | ||||
| 
 | ||||
| func (s *socket) willAcceptEvent(mt EventType) bool { | ||||
| 	if mt == EventTypeError { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	s.mu.RLock() | ||||
| 	_, ok := s.types[mt] | ||||
| 	s.mu.RUnlock() | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (s *socket) setEvent(mt EventType, add bool) { | ||||
| 	s.mu.Lock() | ||||
| 	if add { | ||||
| 		s.types[mt] = struct{}{} | ||||
| 	} else { | ||||
| 		delete(s.types, mt) | ||||
| 	} | ||||
| 	s.mu.Unlock() | ||||
| } | ||||
| 
 | ||||
| func newSocket(ctx context.Context, cancel context.CancelFunc) *socket { | ||||
| 	return &socket{ | ||||
| 		ctx:    ctx, | ||||
| 		cancel: cancel, | ||||
| 		ch:     make(chan Event), | ||||
| 		types:  make(map[EventType]struct{}), | ||||
| 	} | ||||
| } | ||||
|  | @ -9,6 +9,7 @@ import ( | |||
| 	"git.sleepycat.moe/sam/mercury/internal/concurrent" | ||||
| 	"git.sleepycat.moe/sam/mercury/internal/database" | ||||
| 	"git.sleepycat.moe/sam/mercury/internal/database/sql" | ||||
| 	"git.sleepycat.moe/sam/mercury/internal/processor" | ||||
| 	"git.sleepycat.moe/sam/mercury/web/templates" | ||||
| 	"github.com/flosch/pongo2/v6" | ||||
| 	"github.com/go-chi/chi/v5" | ||||
|  | @ -25,6 +26,7 @@ type App struct { | |||
| 	AppConfig config.Config | ||||
| 	DBConfig  *concurrent.Value[database.Config] | ||||
| 	Database  *sql.Base | ||||
| 	Processor *processor.Processor | ||||
| 
 | ||||
| 	tmpl     *pongo2.TemplateSet | ||||
| 	tokenKey []byte | ||||
|  | @ -35,6 +37,7 @@ func NewApp(ctx context.Context, cfg config.Config, db *sql.Base) (*App, error) | |||
| 		Router:    chi.NewRouter(), | ||||
| 		AppConfig: cfg, | ||||
| 		Database:  db, | ||||
| 		Processor: processor.New(db), | ||||
| 	} | ||||
| 
 | ||||
| 	if cfg.Core.SecretKey == "" { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue