code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

Show retract, amend and snapshot on a running serve without a restart

serve rebuilt its output only after a scan found a changed file, so
edits made by another patchwork process (retract, amend, snapshot)
waited for the next upload or a restart. serve now polls SQLite's
data_version every 3 seconds; it moves only when another connection
commits, so serve's own writes don't trigger it and an idle site
doesn't rebuild. Rebuilds are serialized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
author
Pablo Murad
date
commit
744248a475c95d69f6a8dc0d8a795a7faba0f83a
parent
c893eb1e09e37288dd624a126aebd0163145f0bf
changes
4 files +71 −0
@@ -1,5 +1,11 @@ # Changelog
 
+## 0.1.1 (2026-10-24)
+
+### Fixed
+
+- `retract`, `amend` and `snapshot` run from the command line now show up on a running `serve` within a few seconds. Before, `serve` rebuilt its output only after a content file changed, so these edits waited for the next upload or a restart. `serve` now checks SQLite's `data_version` every 3 seconds, a negligible query, and rebuilds only when another process has committed.
+
 ## 0.1.0 (2026-10-24)
 
 First release, running on [moonflowersociety.org](https://moonflowersociety.org).
@@ -7,6 +7,7 @@ "fmt" 	"net"
 	"net/http"
 	"os"
+	"sync"
 	"time"
 
 	"golang.org/x/sync/errgroup"
@@ -39,7 +40,10 @@ *addr = a.cfg.HTTP.Addr 	}
 
 	srv := server.New(a.log)
+	var building sync.Mutex
 	rebuild := func() {
+		building.Lock()
+		defer building.Unlock()
 		start := time.Now()
 		t, err := render.Build(ctx, a.store, a.cfg)
 		if err != nil {
@@ -71,12 +75,39 @@ return nil 		}
 		return err
 	})
+	// retract, amend and snapshot run as separate processes against the same
+	// database. SQLite's data_version moves when they commit; checking it is
+	// a trivial query, so the site shows their changes within seconds without
+	// rebuilding when nothing happened.
+	g.Go(func() error {
+		last, _ := a.store.DataVersion(ctx)
+		t := time.NewTicker(outsideWriteCheck)
+		defer t.Stop()
+		for {
+			select {
+			case <-ctx.Done():
+				return nil
+			case <-t.C:
+				v, err := a.store.DataVersion(ctx)
+				if err != nil || v == last {
+					continue
+				}
+				last = v
+				a.log.Info("database changed by another process")
+				rebuild()
+			}
+		}
+	})
 	g.Go(func() error {
 		a.log.Info("serving", "addr", *addr, "source", a.cfg.Source.Path, "db", a.cfg.DB)
 		return srv.ListenAndServe(ctx, *addr)
 	})
 	return g.Wait()
 }
+
+// outsideWriteCheck is how often serve looks for commits made by other
+// patchwork processes (retract, amend, snapshot).
+const outsideWriteCheck = 3 * time.Second
 
 func changed(rep folder.Report) bool {
 	for _, r := range rep.Results {
@@ -99,6 +99,15 @@ } 	return tx.Commit()
 }
 
+// DataVersion changes whenever another connection commits to the database,
+// such as a `patchwork retract` run while `serve` is up. It does not change
+// for this store's own writes. A cheap query: poll it to notice outside edits.
+func (s *Store) DataVersion(ctx context.Context) (int64, error) {
+	var v int64
+	err := s.db.QueryRowContext(ctx, `PRAGMA data_version`).Scan(&v)
+	return v, err
+}
+
 // Backup writes a consistent copy of the database to path.
 func (s *Store) Backup(ctx context.Context, path string) error {
 	if _, err := os.Stat(path); err == nil {
@@ -103,6 +103,31 @@ t.Fatalf("meta %q", v) 	}
 }
 
+func TestDataVersionSeesOtherConnections(t *testing.T) {
+	ctx := context.Background()
+	path := filepath.Join(t.TempDir(), "dv.db")
+	serve, err := Open(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer serve.Close()
+	cli, err := Open(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cli.Close()
+
+	v0, _ := serve.DataVersion(ctx)
+	serve.SetMeta(ctx, "own", "write")
+	if v, _ := serve.DataVersion(ctx); v != v0 {
+		t.Fatal("own writes must not change data_version")
+	}
+	cli.SetMeta(ctx, "other", "write")
+	if v, _ := serve.DataVersion(ctx); v == v0 {
+		t.Fatal("another connection's commit must change data_version")
+	}
+}
+
 func TestReopenKeepsSchema(t *testing.T) {
 	path := filepath.Join(t.TempDir(), "re.db")
 	s, err := Open(path)