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() }