- every revision bump (API writes and /bmadmin saves) enqueues
updated.jz.bm.material with {material_id, revision} in a savepoint
- the materials namespace authorizer allows a subscribe only to a
visible material the user may manage
- api.ManagesMaterials and api.CanWatchMaterial
93 lines
3.1 KiB
Go
93 lines
3.1 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/lighthouse"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// The live heads-up of a material's revision (D-20): an open editor learns
|
|
// that a newer revision was saved before its own save, which still meets
|
|
// the 409. Only ids and the revision number travel on the channel, never
|
|
// content.
|
|
const (
|
|
// MaterialsNamespace is the realtime namespace jz.bm owns.
|
|
MaterialsNamespace = "materials"
|
|
// MaterialRevisedEvent is the event published after a content write.
|
|
MaterialRevisedEvent = "updated.jz.bm.material"
|
|
)
|
|
|
|
// materialChannelRE is the only accepted form of a material channel:
|
|
// lighthouse.ChannelID reads segments with a PHP-style cast, which would
|
|
// take "materials:5abc" for material 5.
|
|
var materialChannelRE = regexp.MustCompile(`^materials:[1-9][0-9]{0,18}$`)
|
|
|
|
// MaterialChannel is the channel of material id.
|
|
func MaterialChannel(id uint) string {
|
|
return MaterialsNamespace + ":" + strconv.FormatUint(uint64(id), 10)
|
|
}
|
|
|
|
// MaterialChannelID returns the material id of a channel of the form
|
|
// materials:{id}, and false for any other channel (a presence: prefix, a
|
|
// leading zero, trailing characters, a third segment).
|
|
func MaterialChannelID(channel string) (uint, bool) {
|
|
if !materialChannelRE.MatchString(channel) {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseUint(channel[len(MaterialsNamespace)+1:], 10, strconv.IntSize)
|
|
if err != nil || n == 0 {
|
|
return 0, false
|
|
}
|
|
return uint(n), true
|
|
}
|
|
|
|
// MaterialRevised is the payload of MaterialRevisedEvent. The field order
|
|
// is fixed so the payload's key order is stable.
|
|
type MaterialRevised struct {
|
|
MaterialID uint `json:"material_id"`
|
|
Revision uint `json:"revision"`
|
|
}
|
|
|
|
// MaterialRevisionNotifier publishes MaterialRevisedEvent after a write
|
|
// that bumped a material's revision. A nil notifier, or one without a
|
|
// realtime service, publishes nothing.
|
|
type MaterialRevisionNotifier struct {
|
|
// Realtime is the app's lighthouse service; nil turns the event off.
|
|
Realtime *lighthouse.Service
|
|
// Log is where enqueue failures are logged; nil means slog.Default.
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// Revised enqueues the event of material id at revision on the write's
|
|
// transaction tx, so it is published only when that write commits. The
|
|
// enqueue runs in a savepoint: a failure rolls back the savepoint only, is
|
|
// logged and never fails or loses the write. Without a transaction it does
|
|
// nothing.
|
|
func (n *MaterialRevisionNotifier) Revised(ctx context.Context, tx *gorm.DB, id, revision uint) {
|
|
if n == nil || n.Realtime == nil || tx == nil {
|
|
return
|
|
}
|
|
b := lighthouse.Broadcast{
|
|
Channels: []string{MaterialChannel(id)},
|
|
Event: MaterialRevisedEvent,
|
|
Payload: MaterialRevised{MaterialID: id, Revision: revision},
|
|
}
|
|
err := tx.Transaction(func(sp *gorm.DB) error {
|
|
return n.Realtime.Emit(ctx, sp, b)
|
|
})
|
|
if err != nil {
|
|
n.log().Warn("jz.bm: material revision event not queued", "material_id", id, "revision", revision, "error", err)
|
|
}
|
|
}
|
|
|
|
func (n *MaterialRevisionNotifier) log() *slog.Logger {
|
|
if n.Log != nil {
|
|
return n.Log
|
|
}
|
|
return slog.Default()
|
|
}
|