Replies: 2 comments 2 replies
-
I sort of got it working by moving it into a sub package. Don't know if this is good Go practice or styles. If anyone has anything better, please share |
Beta Was this translation helpful? Give feedback.
-
Sending a close messageSo, to solve the first issue – of sending a "close" message – you can look at the complex example. It's not using Echo but the standard library's HTTP library but I believe it should be adaptable by looking at Echo's documentation. Personally I don't have any Echo experience so I'm not able to help you with precise directions on that. Here is the example – the Quick fix to dispatching events after updateFor the GORM issue, an easy – but I would say dirty – solution would be the statement context. When making a GORM query, add the context as described here, but use a context which has the SSE handler as a value. For example: db.WithContext(context.WithValue(ctx, "sseHandler", sseHandler)).Find(...) where This is one way to avoid creating a package for the SSE handler and having that as some sort of singleton – which I would consider an antipattern which gives away code design issues. Read further for an elaboration on that. Some code design stuffNow, for what I would consider a cleaner solution. Normally, to handle a request, a backend server is structured in the following manner:
Let's run through an example: posting a tweet. Here's what the business requirements are:
At the code level, to each of these layers corresponds a struct with methods. For example, the data layer may look like this (I'll be using some pseudocodish Go which ignores imports for the examples): package gormdata // assume the file path for this is data/gormdata/repository.go, it will make sense later
type Repository struct {
db *gorm.DB
}
func (r *Repository) CreateTweet(ctx context.Context, user domain.User, content domain.TweetContent) (domain.Tweet, error)
func (r *Repository) GetUser(ctx context.Context, id domain.ID) (domain.User, error)
func (r *Repository) GetFollowers(ctx context.Context, user domain.User) ([]domain.User, error) The methods above would implement the queries necessary to do what their names describe. Note that there is no if err := db.WithContext(ctx).Find(...).Error; err != nil {
return err // or other appropriate handling
} Anyway, now that the data layer is in place, we should create the business logic/domain layer. Here are the models: package domain
type ID uuid.UUID // assume some external UUID package; could also be int64 or whatever else
type User struct {
ID ID
// other fields, for example a name etc.
}
// TweetContent is a string which can be the content of a tweet.
// That is, a text of length greater than 0 and smaller or equal to 120.
type TweetContent struct {
text string
}
// String returns the inner text as a string.
func (s TweetContent) String() string
// NewTweetContent validates that the input is not empty and not longer than 120 characters
// and returns a TweetContent which contains it. An error is otherwise returned.
func NewTweetContent(input string) (TweetContent, error)
type Tweet struct {
ID ID
Poster *model.User // or maybe just posterID
Content TweetContent
// other fields, for example a createdAt would be useful – not included here
// because it's not specifically stated in our business requirements above.
} Something to note is that we do not consider these models to be GORM-specific here. We aren't and won't be making use of any GORM-specific features – the whole point is to be decoupled from the database outside the data layer. GORM models should probably be inside the As a side note: By creating the Now comes our business logic handler in the same package: package domain
type Repository interface {
CreateTweet(ctx context.Context, user User, content TweetContent) (Tweet, error)
GetUser(ctx context.Context, id ID) (User, error)
GetFollowers(ctx context.Context, user User) ([]User, error)
}
type Domain struct {
repo Repository
sseHandler *sse.Server
}
func (d *Domain) PostTweet(ctx context.Context, userID, content string) (Tweet, error) Note that we define the
Anyway, here's what the
This would be the business logic layer. Now, for the final layer, to route handlers: package handler
type Handler struct {
domain *domain.Domain
}
func (h *Handler) PostTweet(w http.ResponseWriter, r *http.Request) // this method is an http.HandlerFunc, as it will be passed directly to the router. You can use the interface expected by Echo if you're going to use it.
type postTweetInput struct {
UserID string `json:"user_id"`
Content string `json:"content"`
}
type postTweetOutput struct {
TweetID string `json:"tweet_id"`
// maybe other info off the `domain.Tweet` type which could be useful
// to client state management, assuming the API is consumed by an SPA, in React for example. It may look similar to
Note: to distinguish between the error types returned by the domain layer you may have a package domain
type ErrorKind int
const (
ErrorKindInvalidInput ErrorKind = iota
ErrorKindNotFound
ErrorKindInternal
)
type Error struct {
Kind ErrorKind
// Cause is the original error. May be from validation, from the database etc.
Cause error
// other useful fields maybe
}
// Error implements the error interface.
func (e *Error) Error() string
// Unwrap returns e.Cause. It makes this type compatible with errors.As, errors.Unwrap and others.
func (e *Error) Unwrap() error This would be our route handler. In a way you can see how in this case it's just a soft wrapper around the domain layer. If your application is simple and this is too much boilerplate you could omit it and just have the data layer – that still gives you most of the benefits. In the real world, for example, some business logic operations are reused, and by not having the layer code would be duplicated. The separation is also worth it because for example authentication/authorization may be involved and you wouldn't want to execute any business logic for unauthenticated users; by not mixing the two this can be ensured not to happen, as one would authorize first and then go to the domain layer (in code there could be an Finally, how are all these glued up? Here's how the main code could look like: func main() {
gormDB, err := /* init the database */
// handle the error
sseHandler := /* init the SSE handler */
repo := gormdata.New(gormDB) // assume the constructors exists in their respective packages
dom := domain.New(repo, sseHandler)
handler := handler.New(dom)
mux := http.NewServeMux()
// Here handler.PostTweet could be wrapped in middleware, if necessary
mux.HandleFunc("POST /post", handler.PostTweet)
s := &http.Server{
Address: "0.0.0.0:8080",
Handler: mux, // Could also be wrapped in middleware: a panic handler, CORS, logging etc.
// other fields
}
s.RegisterOnShutdown(func() {
// the SSE close message code
})
// maybe add graceful shutdown
err := s.ListenAndServe()
// handle the error
} Note: graceful shutdown is also implemented in the complex example. Take inspiration from that main function for your server startup code. This would be the general outline. How does this long-ass rushed incomplete high-level three tier architecture blog post relate to your Final notesHopefully this helped in giving you some direction on |
Beta Was this translation helpful? Give feedback.
-
Hi, thanks so much for doing this package. It's just what I need at the point I'm at in my project.
I copied from the complex and simple examples.
I have this much working
UPDATE: made sure it works, here's a proof of concept https://github.com/gostega/go-sse-poc
main.go
It works, if I have the random string code uncommented, it will send me random strings every second.
If I have it commented, like above, then I have only the 'Connected' message which I did just to show that the connection was successful.
$ curl localhost:9090/events?topic=evt_statechange id: evt_statechange data: Connected
The part that I'm stuck on now, is publishing messages from elsewhere in my code. I have other modules that do things like update the state on a node. And interact with a database. How do I call `_ = sseHandler.Publish("mymessage") from elsewhere?
For example I have another file
models/node.go
Beta Was this translation helpful? Give feedback.
All reactions