snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill-nats/pkg/nats"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
"github.com/nats-io/stan.go"
|
||||
)
|
||||
|
||||
const (
|
||||
PostCreatedTopic = "post-created"
|
||||
PostUpdatedTopic = "post-updated"
|
||||
FeedUpdatedTopic = "feed-updated"
|
||||
)
|
||||
|
||||
func SetupMessageRouter(
|
||||
feedsStorage FeedsStorage,
|
||||
logger watermill.LoggerAdapter,
|
||||
) (message.Publisher, message.Subscriber, error) {
|
||||
router, err := message.NewRouter(message.RouterConfig{}, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
router.AddMiddleware(middleware.Recoverer)
|
||||
|
||||
natsURL := stan.NatsURL("nats://nats-streaming:4222")
|
||||
pub, err := nats.NewStreamingPublisher(nats.StreamingPublisherConfig{
|
||||
ClusterID: "test-cluster",
|
||||
ClientID: "publisher",
|
||||
StanOptions: []stan.Option{natsURL},
|
||||
Marshaler: nats.GobMarshaler{},
|
||||
}, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sub, err := nats.NewStreamingSubscriber(nats.StreamingSubscriberConfig{
|
||||
ClusterID: "test-cluster",
|
||||
ClientID: "subscriber",
|
||||
StanOptions: []stan.Option{natsURL},
|
||||
Unmarshaler: nats.GobMarshaler{},
|
||||
}, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
router.AddHandler(
|
||||
"update-feeds-on-post-created",
|
||||
PostCreatedTopic,
|
||||
sub,
|
||||
FeedUpdatedTopic,
|
||||
pub,
|
||||
func(msg *message.Message) (messages []*message.Message, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Info("Successfully updated feeds on new post created", nil)
|
||||
} else {
|
||||
logger.Error("Error while updating feeds on new post created", err, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
event := PostCreated{}
|
||||
err = json.Unmarshal(msg.Payload, &event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("Adding post", watermill.LogFields{"post": event.Post})
|
||||
|
||||
if len(event.Post.Tags) > 0 {
|
||||
for _, tag := range event.Post.Tags {
|
||||
logger.Info("Adding tag", watermill.LogFields{"tag": tag})
|
||||
err = feedsStorage.Add(msg.Context(), tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = feedsStorage.AppendPost(msg.Context(), event.Post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return createFeedUpdatedEvents(event.Post.Tags)
|
||||
},
|
||||
)
|
||||
|
||||
router.AddHandler(
|
||||
"update-feeds-on-post-updated",
|
||||
PostUpdatedTopic,
|
||||
sub,
|
||||
FeedUpdatedTopic,
|
||||
pub,
|
||||
func(msg *message.Message) (messages []*message.Message, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Info("Successfully updated feeds on post updated", nil)
|
||||
} else {
|
||||
logger.Error("Error while updating feeds on post updated", err, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
event := PostUpdated{}
|
||||
err = json.Unmarshal(msg.Payload, &event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, tag := range event.NewPost.Tags {
|
||||
logger.Info("Adding tag", watermill.LogFields{"tag": tag})
|
||||
err = feedsStorage.Add(msg.Context(), tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = feedsStorage.UpdatePost(msg.Context(), event.NewPost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createFeedUpdatedEvents(append(event.NewPost.Tags, event.OriginalPost.Tags...))
|
||||
},
|
||||
)
|
||||
|
||||
go func() {
|
||||
err = router.Run(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-router.Running()
|
||||
|
||||
return pub, sub, nil
|
||||
}
|
||||
|
||||
func createFeedUpdatedEvents(tags []string) ([]*message.Message, error) {
|
||||
var messages []*message.Message
|
||||
|
||||
for _, tag := range tags {
|
||||
event := FeedUpdated{
|
||||
Name: tag,
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := message.NewMessage(watermill.NewUUID(), payload)
|
||||
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
type Publisher struct {
|
||||
publisher message.Publisher
|
||||
}
|
||||
|
||||
func (p Publisher) Publish(topic string, event interface{}) error {
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := message.NewMessage(watermill.NewUUID(), payload)
|
||||
|
||||
return p.publisher.Publish(topic, msg)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
const collectionName = "feeds"
|
||||
|
||||
type FeedsStorage struct {
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
func NewFeedsStorage() FeedsStorage {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://root:password@mongo:27017"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = client.Ping(ctx, readpref.Primary())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
db := client.Database("example")
|
||||
names, err := db.ListCollectionNames(ctx, bson.M{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, n := range names {
|
||||
if n == collectionName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err := db.CreateCollection(ctx, collectionName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return FeedsStorage{
|
||||
collection: db.Collection(collectionName),
|
||||
}
|
||||
}
|
||||
|
||||
func (s FeedsStorage) Add(ctx context.Context, name string) error {
|
||||
feed := Feed{
|
||||
Name: name,
|
||||
Posts: []Post{},
|
||||
}
|
||||
|
||||
_, err := s.collection.InsertOne(ctx, feed)
|
||||
if err != nil {
|
||||
if !isDuplicateError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s FeedsStorage) All(ctx context.Context) ([]Feed, error) {
|
||||
cursor, err := s.collection.Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var feeds []Feed
|
||||
|
||||
err = cursor.All(ctx, &feeds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feeds, nil
|
||||
}
|
||||
|
||||
func (s FeedsStorage) ByName(ctx context.Context, name string) (Feed, error) {
|
||||
filter := bson.M{
|
||||
"_id": name,
|
||||
}
|
||||
|
||||
var feed Feed
|
||||
err := s.collection.FindOne(ctx, filter).Decode(&feed)
|
||||
if err != nil {
|
||||
return Feed{}, err
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func (s FeedsStorage) AppendPost(ctx context.Context, post Post) error {
|
||||
return s.appendPostIfNotPresent(ctx, post)
|
||||
}
|
||||
|
||||
func (s FeedsStorage) UpdatePost(ctx context.Context, post Post) error {
|
||||
err := s.updatePostIfPresent(ctx, post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.appendPostIfNotPresent(ctx, post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.removePostIfNotInFeed(ctx, post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s FeedsStorage) updatePostIfPresent(ctx context.Context, post Post) error {
|
||||
if len(post.Tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
"_id": bson.M{
|
||||
"$in": post.Tags,
|
||||
},
|
||||
"posts.id": post.ID,
|
||||
}
|
||||
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"posts.$": post,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s.collection.UpdateMany(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s FeedsStorage) appendPostIfNotPresent(ctx context.Context, post Post) error {
|
||||
if len(post.Tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
"_id": bson.M{
|
||||
"$in": post.Tags,
|
||||
},
|
||||
"posts.id": bson.M{
|
||||
"$ne": post.ID,
|
||||
},
|
||||
}
|
||||
|
||||
update := bson.M{
|
||||
"$push": bson.M{
|
||||
"posts": bson.M{
|
||||
"$each": bson.A{post},
|
||||
"$position": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s.collection.UpdateMany(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s FeedsStorage) removePostIfNotInFeed(ctx context.Context, post Post) error {
|
||||
tags := post.Tags
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
"_id": bson.M{
|
||||
"$nin": tags,
|
||||
},
|
||||
"posts.id": post.ID,
|
||||
}
|
||||
|
||||
update := bson.M{
|
||||
"$pull": bson.M{
|
||||
"posts": bson.M{
|
||||
"id": post.ID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s.collection.UpdateMany(ctx, filter, update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDuplicateError(err error) bool {
|
||||
mErr, ok := err.(mongo.WriteException)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return mErr.WriteErrors[0].Code == 11000
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
module main.go
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/ThreeDotsLabs/watermill-nats v1.0.7
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/nats-io/stan.go v0.10.4
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/ajg/form v1.7.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.4.0 // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||
github.com/hashicorp/raft v1.3.11 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/minio/highwayhash v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.3.0 // indirect
|
||||
github.com/nats-io/nats.go v1.52.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
)
|
||||
196
_examples/real-world-examples/server-sent-events/server/go.sum
Normal file
196
_examples/real-world-examples/server-sent-events/server/go.sum
Normal file
@@ -0,0 +1,196 @@
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ=
|
||||
github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y=
|
||||
github.com/ThreeDotsLabs/watermill-nats v1.0.7 h1:hOquWq0GAwm5jaIc3wGaDoVCPYL+If4NZPb+RUaHni4=
|
||||
github.com/ThreeDotsLabs/watermill-nats v1.0.7/go.mod h1:t5A8XbO/v8CPM+AIljgoO9NR1jBk3ixYBGAtvn1N4lA=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=
|
||||
github.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
|
||||
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/raft v1.3.11 h1:p3v6gf6l3S797NnK5av3HcczOC1T5CLoaRvg0g9ys4A=
|
||||
github.com/hashicorp/raft v1.3.11/go.mod h1:J8naEwc6XaaCfts7+28whSeRvCqTd6e20BlCU3LtEO4=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
|
||||
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI=
|
||||
github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
|
||||
github.com/nats-io/nats-server/v2 v2.6.1 h1:cJy+ia7/4EaJL+ZYDmIy2rD1mDWTfckhtPBU0GYo8xM=
|
||||
github.com/nats-io/nats-server/v2 v2.6.1/go.mod h1:Az91TbZiV7K4a6k/4v6YYdOKEoxCXj+iqhHVf/MlrKo=
|
||||
github.com/nats-io/nats-streaming-server v0.22.1 h1:YKDdLAWZud3UnEBvUPaYppMxSDuh+9czTCDriq19tJY=
|
||||
github.com/nats-io/nats-streaming-server v0.22.1/go.mod h1:1WpVkVV5NyZbHuGGxkaPWopLFnxNthO/TK/BkzFdnPE=
|
||||
github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=
|
||||
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
||||
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw=
|
||||
github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
370
_examples/real-world-examples/server-sent-events/server/http.go
Normal file
370
_examples/real-world-examples/server-sent-events/server/http.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
watermillHTTP "github.com/ThreeDotsLabs/watermill-http/v2/pkg/http"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
var generatedTags = []string{"watermill", "golang", "pubsub", "unicorn", "HelloWorld", "example", "ThreeDotsLabs"}
|
||||
|
||||
type Router struct {
|
||||
Subscriber message.Subscriber
|
||||
Publisher Publisher
|
||||
PostsStorage PostsStorage
|
||||
FeedsStorage FeedsStorage
|
||||
Logger watermill.LoggerAdapter
|
||||
}
|
||||
|
||||
func (router Router) Mux() *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
root := http.Dir("./public")
|
||||
FileServer(r, "/", root)
|
||||
|
||||
sseRouter, err := watermillHTTP.NewSSERouter(
|
||||
watermillHTTP.SSERouterConfig{
|
||||
UpstreamSubscriber: router.Subscriber,
|
||||
ErrorHandler: watermillHTTP.DefaultErrorHandler,
|
||||
},
|
||||
router.Logger,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
postStream := postStreamAdapter{storage: router.PostsStorage, logger: router.Logger}
|
||||
feedStream := feedStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}
|
||||
allFeedsStream := allFeedsStreamAdapter{storage: router.FeedsStorage, logger: router.Logger}
|
||||
|
||||
postHandler := sseRouter.AddHandler(PostUpdatedTopic, postStream)
|
||||
feedHandler := sseRouter.AddHandler(FeedUpdatedTopic, feedStream)
|
||||
allFeedsHandler := sseRouter.AddHandler(FeedUpdatedTopic, allFeedsStream)
|
||||
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/posts/{id}", postHandler)
|
||||
r.Post("/posts", router.CreatePost)
|
||||
r.Post("/generate/post", router.GeneratePost)
|
||||
r.Patch("/posts/{id}", router.UpdatePost)
|
||||
r.Get("/feeds/{name}", feedHandler)
|
||||
r.Get("/feeds", allFeedsHandler)
|
||||
})
|
||||
|
||||
go func() {
|
||||
err = sseRouter.Run(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-sseRouter.Running()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type feedSummary struct {
|
||||
Name string `json:"name"`
|
||||
Posts int `json:"posts"`
|
||||
}
|
||||
|
||||
type AllFeedsResponse struct {
|
||||
Feeds []feedSummary `json:"feeds"`
|
||||
}
|
||||
|
||||
type allFeedsStreamAdapter struct {
|
||||
storage FeedsStorage
|
||||
logger watermill.LoggerAdapter
|
||||
}
|
||||
|
||||
func (f allFeedsStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
|
||||
resp, err := f.getResponse(r)
|
||||
if err != nil {
|
||||
logAndWriteError(f.logger, w, err)
|
||||
return resp, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (f allFeedsStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
|
||||
resp, err := f.getResponse(r)
|
||||
if err != nil {
|
||||
return resp, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (f allFeedsStreamAdapter) getResponse(r *http.Request) (interface{}, error) {
|
||||
feeds, err := f.storage.All(r.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := AllFeedsResponse{
|
||||
Feeds: []feedSummary{},
|
||||
}
|
||||
|
||||
for _, f := range feeds {
|
||||
response.Feeds = append(response.Feeds, feedSummary{
|
||||
Name: f.Name,
|
||||
Posts: len(f.Posts),
|
||||
})
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
type CreatePostRequest struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
func (router Router) CreatePost(w http.ResponseWriter, r *http.Request) {
|
||||
req := CreatePostRequest{}
|
||||
err := render.Decode(r, &req)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
post := NewPost(
|
||||
watermill.NewUUID(),
|
||||
req.Title,
|
||||
req.Content,
|
||||
req.Author,
|
||||
)
|
||||
|
||||
err = router.addPost(r.Context(), post)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (router Router) GeneratePost(w http.ResponseWriter, r *http.Request) {
|
||||
title := gofakeit.Sentence(5)
|
||||
content := gofakeit.Sentence(20)
|
||||
author := gofakeit.Name()
|
||||
|
||||
tagsCount := rand.Intn(3) + 1
|
||||
for i := 0; i < tagsCount; i++ {
|
||||
content += fmt.Sprintf(" #%v", generatedTags[rand.Intn(len(generatedTags))])
|
||||
}
|
||||
|
||||
post := NewPost(
|
||||
watermill.NewUUID(),
|
||||
title,
|
||||
content,
|
||||
author,
|
||||
)
|
||||
|
||||
err := router.addPost(r.Context(), post)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (router Router) addPost(ctx context.Context, post Post) error {
|
||||
err := router.PostsStorage.Add(ctx, post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event := PostCreated{
|
||||
Post: post,
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
err = router.Publisher.Publish(PostCreatedTopic, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdatePostRequest struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (router Router) UpdatePost(w http.ResponseWriter, r *http.Request) {
|
||||
req := UpdatePostRequest{}
|
||||
err := render.Decode(r, &req)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
post, err := router.PostsStorage.ByID(r.Context(), req.ID)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPost := NewPost(
|
||||
post.ID,
|
||||
req.Title,
|
||||
req.Content,
|
||||
post.Author,
|
||||
)
|
||||
|
||||
err = router.PostsStorage.Update(r.Context(), newPost)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
event := PostUpdated{
|
||||
OriginalPost: post,
|
||||
NewPost: newPost,
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
err = router.Publisher.Publish(PostUpdatedTopic, event)
|
||||
if err != nil {
|
||||
logAndWriteError(router.Logger, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
type feedStreamAdapter struct {
|
||||
storage FeedsStorage
|
||||
logger watermill.LoggerAdapter
|
||||
}
|
||||
|
||||
func (f feedStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
|
||||
resp, err := f.getResponse(r)
|
||||
if err != nil {
|
||||
logAndWriteError(f.logger, w, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (f feedStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
|
||||
feedUpdated := FeedUpdated{}
|
||||
|
||||
err := json.Unmarshal(msg.Payload, &feedUpdated)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
feedName := chi.URLParam(r, "name")
|
||||
|
||||
if feedUpdated.Name != feedName {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
resp, err := f.getResponse(r)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (f feedStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {
|
||||
feedName := chi.URLParam(r, "name")
|
||||
|
||||
feed, err := f.storage.ByName(r.Context(), feedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
type postStreamAdapter struct {
|
||||
storage PostsStorage
|
||||
logger watermill.LoggerAdapter
|
||||
}
|
||||
|
||||
func (p postStreamAdapter) InitialStreamResponse(w http.ResponseWriter, r *http.Request) (response interface{}, ok bool) {
|
||||
resp, err := p.getResponse(r)
|
||||
if err != nil {
|
||||
logAndWriteError(p.logger, w, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (p postStreamAdapter) NextStreamResponse(r *http.Request, msg *message.Message) (response interface{}, ok bool) {
|
||||
postUpdated := PostUpdated{}
|
||||
|
||||
err := json.Unmarshal(msg.Payload, &postUpdated)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
postID := chi.URLParam(r, "id")
|
||||
|
||||
if postUpdated.OriginalPost.ID != postID {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
resp, err := p.getResponse(r)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (p postStreamAdapter) getResponse(r *http.Request) (response interface{}, err error) {
|
||||
postID := chi.URLParam(r, "id")
|
||||
|
||||
post, err := p.storage.ByID(r.Context(), postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func FileServer(r chi.Router, path string, root http.FileSystem) {
|
||||
if strings.ContainsAny(path, "{}*") {
|
||||
panic("FileServer does not permit URL parameters.")
|
||||
}
|
||||
|
||||
fs := http.StripPrefix(path, http.FileServer(root))
|
||||
|
||||
if path != "/" && path[len(path)-1] != '/' {
|
||||
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
|
||||
path += "/"
|
||||
}
|
||||
path += "*"
|
||||
|
||||
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func logAndWriteError(logger watermill.LoggerAdapter, w http.ResponseWriter, err error) {
|
||||
logger.Error("Error", err, nil)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := watermill.NewStdLogger(false, false)
|
||||
|
||||
postsStorage := NewPostsStorage()
|
||||
feedsStorage := NewFeedsStorage()
|
||||
|
||||
pub, sub, err := SetupMessageRouter(feedsStorage, logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
httpRouter := Router{
|
||||
Subscriber: sub,
|
||||
Publisher: Publisher{publisher: pub},
|
||||
PostsStorage: postsStorage,
|
||||
FeedsStorage: feedsStorage,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
mux := httpRouter.Mux()
|
||||
|
||||
err = http.ListenAndServe(":8080", mux)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Note that in a real application using both "json" and "bson" tags in the same structure is strongly discouraged.
|
||||
// We use common models for database storage and HTTP API just to make this example simple and easy to grasp.
|
||||
// See our article about the idea behind this: https://threedots.tech/post/things-to-know-about-dry/
|
||||
|
||||
type Post struct {
|
||||
ID string `json:"id" bson:"id"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Content string `json:"content" bson:"content"`
|
||||
Author string `json:"author" bson:"author"`
|
||||
Tags []string `json:"tags" bson:"tags"`
|
||||
}
|
||||
|
||||
func NewPost(id, title, content, author string) Post {
|
||||
pattern := regexp.MustCompile("#([a-zA-Z0-9]+)")
|
||||
matches := pattern.FindAllStringSubmatch(content, -1)
|
||||
|
||||
var tags []string
|
||||
tagsMap := map[string]struct{}{}
|
||||
|
||||
for _, tag := range matches {
|
||||
tagSlug := strings.ToLower(tag[1])
|
||||
|
||||
_, ok := tagsMap[tagSlug]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
tagsMap[tagSlug] = struct{}{}
|
||||
tags = append(tags, tagSlug)
|
||||
}
|
||||
|
||||
return Post{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Author: author,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
type Feed struct {
|
||||
Name string `json:"name" bson:"_id"`
|
||||
Posts []Post `json:"posts" bson:"posts"`
|
||||
}
|
||||
|
||||
type PostCreated struct {
|
||||
Post Post `json:"post"`
|
||||
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type PostUpdated struct {
|
||||
OriginalPost Post `json:"original_post"`
|
||||
NewPost Post `json:"new_post"`
|
||||
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type FeedUpdated struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type PostsStorage struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewPostsStorage() PostsStorage {
|
||||
conf := mysql.NewConfig()
|
||||
conf.Net = "tcp"
|
||||
conf.User = "root"
|
||||
conf.Addr = "mysql"
|
||||
conf.DBName = "example"
|
||||
|
||||
db, err := sql.Open("mysql", conf.FormatDSN())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
break
|
||||
} else {
|
||||
fmt.Println("Could not connect to MySQL, retrying...")
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
}
|
||||
|
||||
return PostsStorage{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (s PostsStorage) ByID(ctx context.Context, id string) (Post, error) {
|
||||
query := "SELECT title, content, author FROM posts WHERE id=?"
|
||||
row := s.db.QueryRowContext(ctx, query, id)
|
||||
|
||||
var title, content, author string
|
||||
err := row.Scan(&title, &content, &author)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
return NewPost(id, title, content, author), nil
|
||||
}
|
||||
|
||||
func (s PostsStorage) Add(ctx context.Context, post Post) error {
|
||||
query := "INSERT INTO posts (id, title, content, author) VALUES (?, ?, ?, ?)"
|
||||
_, err := s.db.ExecContext(ctx, query, post.ID, post.Title, post.Content, post.Author)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s PostsStorage) Update(ctx context.Context, post Post) error {
|
||||
query := "UPDATE posts SET title=?, content=?, author=? WHERE id=?"
|
||||
_, err := s.db.ExecContext(ctx, query, post.Title, post.Content, post.Author, post.ID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Watermill Server-Sent Events Example</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
.example {
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
#feeds li {
|
||||
display: inline;
|
||||
}
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "|";
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||
<a class="navbar-brand" href="/">Home</a>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="container">
|
||||
<div class="example">
|
||||
<feeds-list></feeds-list>
|
||||
<add-post></add-post>
|
||||
|
||||
<hr />
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.7.16/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@3.6.5/dist/vue-router.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.3.5"></script>
|
||||
|
||||
<script type="text/x-template" id="feeds-list">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item" v-bind:class="{ active: feed.active }" v-for="feed in feeds">
|
||||
<router-link v-if="!feed.active" :to="{ name: 'show-feed', params: { feed_name: feed.name }}">
|
||||
{{ feed.text }}
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ feed.text }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="add-post">
|
||||
<div>
|
||||
<p>
|
||||
<button class="btn btn-primary" type="button" v-on:click="toggle">
|
||||
Add new post
|
||||
</button>
|
||||
|
||||
<button class="btn btn-info" type="button" v-on:click="generatePost">Add randomly generated post</button>
|
||||
</p>
|
||||
<div id="addPostForm" v-show="showForm">
|
||||
<div class="card card-body">
|
||||
<h2>Add new post</h2>
|
||||
<form v-on:submit.prevent="submitNewPost">
|
||||
<div class="form-group row">
|
||||
<label for="inputTitle" class="col-sm-2 col-form-label">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="title" class="form-control" id="inputTitle" placeholder="Title" v-model="post.title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="inputAuthor" class="col-sm-2 col-form-label">Author</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="author" class="form-control" id="inputAuthor" placeholder="Author" v-model="post.author">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="inputContent" class="col-sm-2 col-form-label">Content</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="inputContent" rows="3" v-model="post.content"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-10">
|
||||
<input type="submit" class="btn btn-success" value="Add post" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="home">
|
||||
<div>
|
||||
<h3>1. Add some posts using the buttons above (use #hashtags).</h3>
|
||||
<h3>2. Click one of feeds to see a live stream.</h3>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="feed-template">
|
||||
<div>
|
||||
<h2>{{ feed }}</h2>
|
||||
<div v-for="post in posts">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">
|
||||
{{post.title}}
|
||||
<router-link :to="{ name: 'edit-post', params: { post_id: post.id }}"
|
||||
class="btn btn-light class-right btn-sm">Edit
|
||||
</router-link>
|
||||
</h4>
|
||||
<p class="card-text" v-html="post.content"></p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">Author: {{post.author}}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="post-edit-template">
|
||||
<div>
|
||||
<h2>Edit post</h2>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form v-on:submit.prevent="submitData">
|
||||
<div class="form-group row">
|
||||
<label for="inputTitle" class="col-sm-2 col-form-label">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="title" class="form-control" id="inputTitle" placeholder="Title" v-model="post.title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="inputContent" class="col-sm-2 col-form-label">Content</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="inputContent" rows="3" v-model="post.content"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-10">
|
||||
<input type="submit" class="btn btn-success" value="Update post" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
Vue.component('feeds-list', {
|
||||
template: '#feeds-list',
|
||||
data: function () {
|
||||
return {
|
||||
feeds_stream: [],
|
||||
current_feed: "",
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.es = new EventSource('/api/feeds')
|
||||
|
||||
this.es.addEventListener('data', event => {
|
||||
let data = JSON.parse(event.data);
|
||||
this.feeds_stream = data.feeds;
|
||||
}, false);
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (to.name === "show-feed") {
|
||||
this.current_feed = to.params.feed_name;
|
||||
} else {
|
||||
this.current_feed = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
feeds: function (){
|
||||
return this.feeds_stream.map(f => ({text: `${f.name} (${f.posts})`, name: f.name, active: f.name === this.current_feed}));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Home = {
|
||||
template: '#home',
|
||||
}
|
||||
|
||||
Vue.component('add-post', {
|
||||
template: '#add-post',
|
||||
data: function () {
|
||||
return {
|
||||
post: {},
|
||||
showForm: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitNewPost: function() {
|
||||
postData = this.post;
|
||||
this.$http.post('/api/posts', JSON.stringify(postData))
|
||||
this.post = {};
|
||||
},
|
||||
generatePost: function() {
|
||||
this.$http.post('/api/generate/post')
|
||||
},
|
||||
toggle: function () {
|
||||
this.showForm = !this.showForm;
|
||||
},
|
||||
},
|
||||
created() {},
|
||||
});
|
||||
|
||||
const ShowFeed = {
|
||||
template: '#feed-template',
|
||||
data: function () {
|
||||
return {
|
||||
feed: "",
|
||||
posts_stream: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
console.log(to)
|
||||
this.setupFeed(to.params.feed_name);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setupFeed(this.$route.params.feed_name);
|
||||
},
|
||||
destroyed() {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setupFeed: function(feed_name) {
|
||||
this.feed = feed_name;
|
||||
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
}
|
||||
this.es = new EventSource('/api/feeds/' + this.feed)
|
||||
|
||||
this.es.addEventListener('data', event => {
|
||||
let data = JSON.parse(event.data);
|
||||
this.posts_stream = data.posts;
|
||||
}, false);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
posts: function() {
|
||||
var posts = this.posts_stream.slice();
|
||||
for (var p of posts) {
|
||||
p.content = p.content.replace(/#([\w]+)/g,'<a href="#/feed/$1">#$1</a>')
|
||||
}
|
||||
return posts;
|
||||
},
|
||||
},
|
||||
};
|
||||
const EditPost = {
|
||||
template: "#post-edit-template",
|
||||
data: function () {
|
||||
return {
|
||||
post_id: this.$route.params.post_id,
|
||||
post: {},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setupPost();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.es) {
|
||||
console.log("closing")
|
||||
this.es.close();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitData: function () {
|
||||
postData = this.post;
|
||||
this.$http.patch('/api/posts/' + this.post_id, JSON.stringify(postData));
|
||||
router.go(-1);
|
||||
},
|
||||
setupPost() {
|
||||
this.es = new EventSource('/api/posts/' + this.post_id)
|
||||
|
||||
this.es.addEventListener('data', event => {
|
||||
let data = JSON.parse(event.data);
|
||||
this.post = data;
|
||||
}, false);
|
||||
|
||||
this.es.addEventListener('error', function (e) {
|
||||
if (e.readyState == EventSource.CLOSED) {
|
||||
console.log("close", e)
|
||||
} else {
|
||||
console.log("other error", e)
|
||||
}
|
||||
}, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const routes = [
|
||||
{path: '/', component: Home, name: 'home'},
|
||||
{path: '/edit-post/:post_id', component: EditPost, name: 'edit-post'},
|
||||
{path: '/feed/:feed_name', component: ShowFeed, name: 'show-feed'},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
const app = new Vue({
|
||||
router
|
||||
}).$mount('#app')
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user