diff --git a/knotserver/db/oplog.go b/knotserver/db/oplog.go index 7672a9d..842886c 100644 --- a/knotserver/db/oplog.go +++ b/knotserver/db/oplog.go @@ -2,6 +2,8 @@ package db import ( "fmt" + + "tangled.sh/tangled.sh/core/knotserver/notifier" ) type Op struct { @@ -13,7 +15,7 @@ type Op struct { Ref string // the reference being updated } -func (d *DB) InsertOp(op Op) error { +func (d *DB) InsertOp(op Op, notifier *notifier.Notifier) error { _, err := d.db.Exec( `insert into oplog (tid, did, repo, old_sha, new_sha, ref) values (?, ?, ?, ?, ?, ?)`, op.Tid, @@ -23,7 +25,12 @@ func (d *DB) InsertOp(op Op) error { op.NewSha, op.Ref, ) - return err + if err != nil { + return err + } + + notifier.NotifyAll() + return nil } func (d *DB) GetOps(cursor string) ([]Op, error) { diff --git a/knotserver/handler.go b/knotserver/handler.go index 7838afd..2e0bdbc 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -11,6 +11,7 @@ import ( "tangled.sh/tangled.sh/core/jetstream" "tangled.sh/tangled.sh/core/knotserver/config" "tangled.sh/tangled.sh/core/knotserver/db" + "tangled.sh/tangled.sh/core/knotserver/notifier" "tangled.sh/tangled.sh/core/rbac" ) @@ -24,6 +25,7 @@ type Handle struct { jc *jetstream.JetstreamClient e *rbac.Enforcer l *slog.Logger + n *notifier.Notifier // init is a channel that is closed when the knot has been initailized // i.e. when the first user (knot owner) has been added. @@ -31,7 +33,7 @@ type Handle struct { knotInitialized bool } -func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger) (http.Handler, error) { +func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { r := chi.NewRouter() h := Handle{ @@ -40,6 +42,7 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j e: e, l: l, jc: jc, + n: n, init: make(chan struct{}), } diff --git a/knotserver/internal.go b/knotserver/internal.go index 0fcfb2b..bccffed 100644 --- a/knotserver/internal.go +++ b/knotserver/internal.go @@ -12,6 +12,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "tangled.sh/tangled.sh/core/knotserver/config" "tangled.sh/tangled.sh/core/knotserver/db" + "tangled.sh/tangled.sh/core/knotserver/notifier" "tangled.sh/tangled.sh/core/rbac" ) @@ -20,6 +21,7 @@ type InternalHandle struct { c *config.Config e *rbac.Enforcer l *slog.Logger + n *notifier.Notifier } func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) { @@ -99,7 +101,7 @@ func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) } for _, op := range ops { - err := h.db.InsertOp(op) + err := h.db.InsertOp(op, h.n) if err != nil { l.Error("failed to insert op", "err", err, "op", op) continue @@ -109,7 +111,7 @@ func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) return } -func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger) http.Handler { +func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler { r := chi.NewRouter() h := InternalHandle{ @@ -117,6 +119,7 @@ func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer c, e, l, + n, } r.Get("/push-allowed", h.PushAllowed) diff --git a/knotserver/notifier/notifier.go b/knotserver/notifier/notifier.go new file mode 100644 index 0000000..c4ff996 --- /dev/null +++ b/knotserver/notifier/notifier.go @@ -0,0 +1,43 @@ +package notifier + +import ( + "sync" +) + +type Notifier struct { + subscribers map[chan struct{}]struct{} + mu sync.Mutex +} + +func New() Notifier { + return Notifier{ + subscribers: make(map[chan struct{}]struct{}), + } +} + +func (n *Notifier) Subscribe() chan struct{} { + ch := make(chan struct{}, 1) + n.mu.Lock() + n.subscribers[ch] = struct{}{} + n.mu.Unlock() + return ch +} + +func (n *Notifier) Unsubscribe(ch chan struct{}) { + n.mu.Lock() + delete(n.subscribers, ch) + close(ch) + n.mu.Unlock() +} + +func (n *Notifier) NotifyAll() { + n.mu.Lock() + for ch := range n.subscribers { + select { + case ch <- struct{}{}: + default: + // avoid blocking if channel is full + } + } + n.mu.Unlock() +} diff --git a/knotserver/server.go b/knotserver/server.go index 7cb155a..bca1caf 100644 --- a/knotserver/server.go +++ b/knotserver/server.go @@ -11,6 +11,7 @@ import ( "tangled.sh/tangled.sh/core/jetstream" "tangled.sh/tangled.sh/core/knotserver/config" "tangled.sh/tangled.sh/core/knotserver/db" + "tangled.sh/tangled.sh/core/knotserver/notifier" "tangled.sh/tangled.sh/core/log" "tangled.sh/tangled.sh/core/rbac" ) @@ -79,11 +80,14 @@ func Run(ctx context.Context, cmd *cli.Command) error { logger.Error("failed to setup jetstream", "error", err) } - mux, err := Setup(ctx, c, db, e, jc, logger) + notifier := notifier.New() + + mux, err := Setup(ctx, c, db, e, jc, logger, ¬ifier) if err != nil { return fmt.Errorf("failed to setup server: %w", err) } - imux := Internal(ctx, c, db, e, iLogger) + + imux := Internal(ctx, c, db, e, iLogger, ¬ifier) logger.Info("starting internal server", "address", c.Server.InternalListenAddr) go http.ListenAndServe(c.Server.InternalListenAddr, imux)