feat: purge the site cache after SEO writes
- every write that records history (admin, MCP, batch, import, undo, automatic) queues the framework's site-cache purge (kind seo, slug the address or * for settings and robots.txt) after its commit, once per transaction; a rolled-back write never purges - the record-deleted cleanup and the address tracking of saves outside the admin run in lagoon transactions, so their history purges as well
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/conga"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -193,7 +194,9 @@ func (s *Service) RecordDeleted(ctx context.Context, ev *pact.RecordDeleted) err
|
||||
return err
|
||||
}
|
||||
ctx = WithVia(ctx, models.ViaAuto)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// A lagoon transaction, so the history it records purges the site
|
||||
// cache after the commit.
|
||||
return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
entries, err := LoadEntries(tx, typ.Content.Model, ev.ID)
|
||||
if err != nil || len(entries) == 0 {
|
||||
return err
|
||||
|
||||
@@ -7,10 +7,13 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cooler"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"gorm.io/gorm"
|
||||
@@ -232,10 +235,82 @@ func (s *Service) Record(ctx context.Context, db *gorm.DB, changes ...Change) er
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.purgeAfterCommit(ctx, db, purgeSlug(c))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SiteCacheKind is the purge kind of SEO writes (P33-19).
|
||||
const SiteCacheKind = "seo"
|
||||
|
||||
// purgeAfterCommit queues the rendered site's cache purge once the write
|
||||
// db belongs to commits (P33-19, D-97): every SEO write that records
|
||||
// history (admin, MCP, batch, import, undo, automatic) refreshes the
|
||||
// layer's cached resolve answers and pages. A transaction purges once,
|
||||
// however many rows it records; a rolled-back write never purges. Without
|
||||
// a configured purger (site_cache.purge_url) it does nothing.
|
||||
func (s *Service) purgeAfterCommit(ctx context.Context, db *gorm.DB, slug string) {
|
||||
if s == nil || s.app == nil {
|
||||
return
|
||||
}
|
||||
p := cooler.SiteCacheFromApp(s.app)
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
var pool gorm.ConnPool
|
||||
if db != nil && db.Statement != nil {
|
||||
if _, inTx := db.Statement.ConnPool.(gorm.TxCommitter); inTx {
|
||||
pool = db.Statement.ConnPool
|
||||
}
|
||||
}
|
||||
if pool != nil {
|
||||
purgeMu.Lock()
|
||||
if purging == nil || len(purging) > maxPendingPurges {
|
||||
// Rolled-back transactions leave their key behind; the map is
|
||||
// reset rather than grow.
|
||||
purging = map[gorm.ConnPool]bool{}
|
||||
}
|
||||
if purging[pool] {
|
||||
purgeMu.Unlock()
|
||||
return
|
||||
}
|
||||
purging[pool] = true
|
||||
purgeMu.Unlock()
|
||||
lagoon.AfterCommit(ctx, db, func(context.Context, *gorm.DB) {
|
||||
purgeMu.Lock()
|
||||
delete(purging, pool)
|
||||
purgeMu.Unlock()
|
||||
})
|
||||
}
|
||||
cooler.PurgeSiteCacheAfterCommit(ctx, db, p, SiteCacheKind, slug)
|
||||
}
|
||||
|
||||
// maxPendingPurges caps the transactions purgeAfterCommit remembers.
|
||||
const maxPendingPurges = 1024
|
||||
|
||||
// purging holds the open transactions whose site-cache purge is queued, so
|
||||
// each purges once whichever Service instance records in it.
|
||||
var (
|
||||
purgeMu sync.Mutex
|
||||
purging map[gorm.ConnPool]bool
|
||||
)
|
||||
|
||||
// purgeSlug is the purge's slug: "*" for the settings and robots.txt,
|
||||
// else the changed object's address.
|
||||
func purgeSlug(c Change) string {
|
||||
if c.Kind == models.SubjectSettings || c.Kind == models.SubjectRobots {
|
||||
return "*"
|
||||
}
|
||||
for _, values := range []map[string]any{c.After, c.Before} {
|
||||
for _, key := range []string{"url", "source"} {
|
||||
if v, ok := values[key].(string); ok && strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
return "*"
|
||||
}
|
||||
|
||||
// translator returns the application translator, nil when none.
|
||||
func (s *Service) translator() *phrasebook.Translator {
|
||||
if s == nil || s.app == nil {
|
||||
|
||||
Reference in New Issue
Block a user