diff --git a/backend/app/api/app.go b/backend/app/api/app.go index 16c0ea9..73d7809 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -4,6 +4,7 @@ import ( "time" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/sys/config" @@ -18,6 +19,7 @@ type app struct { server *server.Server repos *repo.AllRepos services *services.AllServices + bus *eventbus.EventBus } func new(conf *config.Config) *app { diff --git a/backend/app/api/handlers/v1/controller.go b/backend/app/api/handlers/v1/controller.go index 801eca8..a30f977 100644 --- a/backend/app/api/handlers/v1/controller.go +++ b/backend/app/api/handlers/v1/controller.go @@ -3,10 +3,15 @@ package v1 import ( "net/http" + "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/server" + "github.com/rs/zerolog/log" + + "github.com/olahol/melody" ) type Results[T any] struct { @@ -49,6 +54,7 @@ type V1Controller struct { maxUploadSize int64 isDemo bool allowRegistration bool + bus *eventbus.EventBus } type ( @@ -77,11 +83,12 @@ func BaseUrlFunc(prefix string) func(s string) string { } } -func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ...func(*V1Controller)) *V1Controller { +func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller { ctrl := &V1Controller{ repo: repos, svc: svc, allowRegistration: true, + bus: bus, } for _, opt := range options { @@ -110,3 +117,52 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.Hand }) } } + +func (ctrl *V1Controller) HandleCacheWS() errchain.HandlerFunc { + m := melody.New() + + m.HandleConnect(func(s *melody.Session) { + auth := services.NewContext(s.Request.Context()) + s.Set("gid", auth.GID) + }) + + ctrl.bus.Subscribe(eventbus.EventLabelMutation, func(data any) { + eventData, ok := data.(eventbus.GroupMutationEvent) + if !ok { + log.Log().Msgf("invalid event data: %v", data) + return + } + + _ = m.BroadcastFilter([]byte(`{"event": "label.mutation"}`), func(s *melody.Session) bool { + gidStr, ok := s.Get("gid") + if !ok { + return false + } + + gid := gidStr.(uuid.UUID) + return gid == eventData.GID + }) + }) + + ctrl.bus.Subscribe(eventbus.EventLocationMutation, func(data any) { + eventData, ok := data.(eventbus.GroupMutationEvent) + if !ok { + log.Log().Msgf("invalid event data: %v", data) + return + } + + _ = m.BroadcastFilter([]byte(`{"event": "location.mutation"}`), func(s *melody.Session) bool { + gidStr, ok := s.Get("gid") + if !ok { + return false + } + + gid := gidStr.(uuid.UUID) + return gid == eventData.GID + }) + }) + + return func(w http.ResponseWriter, r *http.Request) error { + return m.HandleRequest(w, r) + } +} diff --git a/backend/app/api/main.go b/backend/app/api/main.go index befc0de..49cb00e 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/migrations" "github.com/hay-kot/homebox/backend/internal/data/repo" @@ -116,8 +117,9 @@ func run(cfg *config.Config) error { return err } + app.bus = eventbus.New() app.db = c - app.repos = repo.New(c, cfg.Storage.Data) + app.repos = repo.New(c, app.bus, cfg.Storage.Data) app.services = services.New( app.repos, services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID), @@ -150,6 +152,8 @@ func run(cfg *config.Config) error { // ========================================================================= // Start Reoccurring Tasks + go app.bus.Run() + go app.startBgTask(time.Duration(24)*time.Hour, func() { _, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background()) if err != nil { diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index f0b89f1..0958341 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -51,6 +51,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR v1Ctrl := v1.NewControllerV1( a.services, a.repos, + a.bus, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize), v1.WithRegistration(a.conf.Options.AllowRegistration), v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode @@ -70,6 +71,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR a.mwRoles(RoleModeOr, authroles.RoleUser.String()), } + r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...)) r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...)) r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...)) r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...)) @@ -153,7 +155,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...)) r.NotFound(chain.ToHandlerFunc(notFoundHandler())) - } func registerMimes() { diff --git a/backend/go.mod b/backend/go.mod index 454a129..4b53c7b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -14,6 +14,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/hay-kot/httpkit v0.0.3 github.com/mattn/go-sqlite3 v1.14.17 + github.com/olahol/melody v1.1.4 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.1 github.com/stretchr/testify v1.8.4 @@ -43,6 +44,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect diff --git a/backend/go.sum b/backend/go.sum index f1be64c..c616e2b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -410,6 +410,8 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY= @@ -511,8 +513,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -538,8 +538,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E= +github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -611,10 +611,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/backend/internal/core/services/reporting/eventbus/eventbus.go b/backend/internal/core/services/reporting/eventbus/eventbus.go new file mode 100644 index 0000000..8cd794e --- /dev/null +++ b/backend/internal/core/services/reporting/eventbus/eventbus.go @@ -0,0 +1,83 @@ +// / Package eventbus provides an interface for event bus. +package eventbus + +import ( + "sync" + + "github.com/google/uuid" +) + +type Event string + +const ( + EventLabelMutation Event = "label.mutation" + EventLocationMutation Event = "location.mutation" +) + +type GroupMutationEvent struct { + GID uuid.UUID +} + +type eventData struct { + event Event + data any +} + +type EventBus struct { + started bool + ch chan eventData + + mu sync.RWMutex + subscribers map[Event][]func(any) +} + +func New() *EventBus { + return &EventBus{ + ch: make(chan eventData, 10), + subscribers: map[Event][]func(any){ + EventLabelMutation: {}, + EventLocationMutation: {}, + }, + } +} + +func (e *EventBus) Run() { + if e.started { + panic("event bus already started") + } + + e.started = true + + for event := range e.ch { + e.mu.RLock() + arr, ok := e.subscribers[event.event] + e.mu.RUnlock() + + if !ok { + continue + } + + for _, fn := range arr { + fn(event.data) + } + } +} + +func (e *EventBus) Publish(event Event, data any) { + e.ch <- eventData{ + event: event, + data: data, + } +} + +func (e *EventBus) Subscribe(event Event, fn func(any)) { + e.mu.Lock() + defer e.mu.Unlock() + + arr, ok := e.subscribers[event] + if !ok { + panic("event not found") + } + + e.subscribers[event] = append(arr, fn) +} diff --git a/backend/internal/data/repo/repo_labels.go b/backend/internal/data/repo/repo_labels.go index ee62fd8..e9ad303 100644 --- a/backend/internal/data/repo/repo_labels.go +++ b/backend/internal/data/repo/repo_labels.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/item" @@ -13,8 +14,10 @@ import ( ) type LabelRepository struct { - db *ent.Client + db *ent.Client + bus *eventbus.EventBus } + type ( LabelCreate struct { Name string `json:"name" validate:"required,min=1,max=255"` @@ -65,6 +68,12 @@ func mapLabelOut(label *ent.Label) LabelOut { } } +func (r *LabelRepository) publishMutationEvent(GID uuid.UUID) { + if r.bus != nil { + r.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: GID}) + } +} + func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label) (LabelOut, error) { return mapLabelOutErr(r.db.Label.Query(). Where(where...). @@ -105,6 +114,7 @@ func (r *LabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data L } label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID + r.publishMutationEvent(groupdId) return mapLabelOut(label), err } @@ -136,6 +146,7 @@ func (r *LabelRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data return LabelOut{}, err } + r.publishMutationEvent(GID) return r.GetOne(ctx, data.ID) } @@ -149,6 +160,11 @@ func (r *LabelRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) label.ID(id), label.HasGroupWith(group.ID(gid)), ).Exec(ctx) + if err != nil { + return err + } - return err + r.publishMutationEvent(gid) + + return nil } diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 28e3968..12b7e97 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/item" @@ -14,7 +15,8 @@ import ( ) type LocationRepository struct { - db *ent.Client + db *ent.Client + bus *eventbus.EventBus } type ( @@ -90,6 +92,12 @@ func mapLocationOut(location *ent.Location) LocationOut { } } +func (r *LocationRepository) publishMutationEvent(GID uuid.UUID) { + if r.bus != nil { + r.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: GID}) + } +} + type LocationQuery struct { FilterChildren bool `json:"filterChildren" schema:"filterChildren"` } @@ -190,6 +198,7 @@ func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data Loc } location.Edges.Group = &ent.Group{ID: GID} // bootstrap group ID + r.publishMutationEvent(GID) return mapLocationOut(location), nil } @@ -218,7 +227,13 @@ func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (L } func (r *LocationRepository) UpdateByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) { - return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) + v, err := r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) + if err != nil { + return LocationOut{}, err + } + + r.publishMutationEvent(GID) + return v, err } func (r *LocationRepository) Delete(ctx context.Context, ID uuid.UUID) error { @@ -227,6 +242,11 @@ func (r *LocationRepository) Delete(ctx context.Context, ID uuid.UUID) error { func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUID) error { _, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx) + if err != nil { + return err + } + r.publishMutationEvent(GID) + return err } diff --git a/backend/internal/data/repo/repos_all.go b/backend/internal/data/repo/repos_all.go index 2a3cf27..4752e8b 100644 --- a/backend/internal/data/repo/repos_all.go +++ b/backend/internal/data/repo/repos_all.go @@ -1,6 +1,9 @@ package repo -import "github.com/hay-kot/homebox/backend/internal/data/ent" +import ( + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" + "github.com/hay-kot/homebox/backend/internal/data/ent" +) // AllRepos is a container for all the repository interfaces type AllRepos struct { @@ -16,13 +19,13 @@ type AllRepos struct { Notifiers *NotifierRepository } -func New(db *ent.Client, root string) *AllRepos { +func New(db *ent.Client, bus *eventbus.EventBus, root string) *AllRepos { return &AllRepos{ Users: &UserRepository{db}, AuthTokens: &TokenRepository{db}, Groups: NewGroupRepository(db), - Locations: &LocationRepository{db}, - Labels: &LabelRepository{db}, + Locations: &LocationRepository{db, bus}, + Labels: &LabelRepository{db, bus}, Items: &ItemsRepository{db}, Docs: &DocumentRepository{db, root}, Attachments: &AttachmentRepo{db}, diff --git a/backend/internal/web/mid/logger.go b/backend/internal/web/mid/logger.go index d087c68..0be4722 100644 --- a/backend/internal/web/mid/logger.go +++ b/backend/internal/web/mid/logger.go @@ -1,6 +1,9 @@ package mid import ( + "bufio" + "errors" + "net" "net/http" "github.com/go-chi/chi/v5/middleware" @@ -17,6 +20,14 @@ func (s *spy) WriteHeader(status int) { s.ResponseWriter.WriteHeader(status) } +func (s *spy) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := s.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, errors.New("response writer does not support hijacking") + } + return hj.Hijack() +} + func Logger(l zerolog.Logger) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/frontend/components/Label/CreateModal.vue b/frontend/components/Label/CreateModal.vue index 4b0ee48..f97e3f0 100644 --- a/frontend/components/Label/CreateModal.vue +++ b/frontend/components/Label/CreateModal.vue @@ -29,7 +29,7 @@

- use Shift + Enter to create and add another + use Shift + Enter to create and add another

diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 8941b28..17645f5 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -174,52 +174,30 @@ }, ]; - function isMutation(method: string | undefined) { - return method === "POST" || method === "PUT" || method === "DELETE"; - } - function isSuccess(status: number) { - return status >= 200 && status < 300; - } - const labelStore = useLabelStore(); - const reLabel = /\/api\/v1\/labels\/.*/gm; - const rmLabelStoreObserver = defineObserver("labelStore", { - handler: (resp, req) => { - if (isMutation(req?.method) && isSuccess(resp.status) && resp.url.match(reLabel)) { - labelStore.refresh(); - } - console.debug("labelStore handler called by observer"); - }, - }); const locationStore = useLocationStore(); - const reLocation = /\/api\/v1\/locations\/.*/gm; - const rmLocationStoreObserver = defineObserver("locationStore", { - handler: (resp, req) => { - if (isMutation(req?.method) && isSuccess(resp.status) && resp.url.match(reLocation)) { - locationStore.refreshChildren(); - locationStore.refreshParents(); + + type EventMessage = { + event: string; + }; + + onMounted(() => { + const ws = new WebSocket(`ws://${window.location.host}/api/v1/ws/events`); + ws.onmessage = event => { + const msg: EventMessage = JSON.parse(event.data); + switch (msg.event) { + case "label.mutation": + console.log("label.mutation"); + labelStore.refresh(); + break; + case "location.mutation": + console.log("location.mutation"); + locationStore.refreshChildren(); + locationStore.refreshParents(); + break; } - - console.debug("locationStore handler called by observer"); - }, - }); - - const eventBus = useEventBus(); - eventBus.on( - EventTypes.InvalidStores, - () => { - labelStore.refresh(); - locationStore.refreshChildren(); - locationStore.refreshParents(); - }, - "stores" - ); - - onUnmounted(() => { - rmLabelStoreObserver(); - rmLocationStoreObserver(); - eventBus.off(EventTypes.InvalidStores, "stores"); + }; }); const authCtx = useAuthContext(); diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 479d1f4..b54cf71 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -3,11 +3,12 @@ import { defineNuxtConfig } from "nuxt/config"; // https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ ssr: false, - modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt", "@vite-pwa/nuxt"], + modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt", "@vite-pwa/nuxt", "./nuxt.proxyoverride.ts"], nitro: { devProxy: { "/api": { target: "http://localhost:7745/api", + ws: true, changeOrigin: true, }, }, diff --git a/frontend/nuxt.proxyoverride.ts b/frontend/nuxt.proxyoverride.ts new file mode 100644 index 0000000..bcf60ed --- /dev/null +++ b/frontend/nuxt.proxyoverride.ts @@ -0,0 +1,45 @@ +// https://gist.github.com/ucw/67f7291c64777fb24341e8eae72bcd24 +import { IncomingMessage } from "http"; +import internal from "stream"; +import { defineNuxtModule, logger } from "@nuxt/kit"; +import { createProxyServer } from "http-proxy"; + +export default defineNuxtModule({ + defaults: { + target: "ws://localhost:7745", + path: "/api/v1/ws", + }, + meta: { + configKey: "websocketProxy", + name: "Websocket proxy", + }, + setup(resolvedOptions, nuxt) { + if (!nuxt.options.dev || !resolvedOptions.target) { + return; + } + + nuxt.hook("listen", server => { + const proxy = createProxyServer({ + ws: true, + secure: false, + changeOrigin: true, + target: resolvedOptions.target, + }); + + const proxyFn = (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => { + if (req.url && req.url.startsWith(resolvedOptions.path)) { + proxy.ws(req, socket, head); + } + }; + + server.on("upgrade", proxyFn); + + nuxt.hook("close", () => { + server.off("upgrade", proxyFn); + proxy.close(); + }); + + logger.info(`Websocket dev proxy started on ${resolvedOptions.path}`); + }); + }, +}); diff --git a/frontend/package.json b/frontend/package.json index 6988142..1785612 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "chart.js": "^4.0.1", "daisyui": "^2.24.0", "dompurify": "^3.0.0", + "h3": "^1.7.1", "markdown-it": "^13.0.1", "pinia": "^2.0.21", "postcss": "^8.4.16", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index cb50ab4..5a7de56 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: dompurify: specifier: ^3.0.0 version: 3.0.0 + h3: + specifier: ^1.7.1 + version: 1.7.1 markdown-it: specifier: ^13.0.1 version: 13.0.1