feat(web): Processor port + in-flight ProcessingSet on App
Add the seam for immediate web-triggered summarization. Processor is the optional single-video summarize port (nil = queue-only, unchanged behaviour); ProcessingSet is an ephemeral, concurrency-safe set of in-flight (user,video) ids so a status endpoint can show progress until the summary lands. State is deliberately in-memory only — the DB holds the durable truth across restarts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,13 @@ type App struct {
|
||||
// Secrets removes a user's OAuth tokens on disconnect / delete-account. The
|
||||
// account routes require it; cmd/tapir wires the file-backed store.
|
||||
Secrets SecretRemover
|
||||
// Processor, when non-nil, summarizes a queued video immediately in a
|
||||
// background goroutine (the "Summarize" button kicks it off). Nil = queue-only:
|
||||
// the button flips the DB flag and the next `tapir run` does the work.
|
||||
Processor Processor
|
||||
// Processing tracks in-flight immediate summarizations so the status endpoint
|
||||
// shows the animation until the summary lands. The zero value is ready to use.
|
||||
Processing ProcessingSet
|
||||
}
|
||||
|
||||
func (a *App) logger() *slog.Logger {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Processor runs the core summarization use case for a single already-discovered
|
||||
// video — resolve its transcript, summarize, deliver to the store. *usecase.Engine
|
||||
// wrapped with the store satisfies it (wired in cmd/tapir). Optional on App: a nil
|
||||
// Processor means queue-only — the "Summarize" button only flips the DB flag and
|
||||
// the next `tapir run` does the work.
|
||||
type Processor interface {
|
||||
ProcessVideo(ctx context.Context, userID, videoID string) error
|
||||
}
|
||||
|
||||
// ProcessingSet tracks the (user, video) ids currently being summarized in-process
|
||||
// so the status endpoint can show the animation until the summary lands. It is
|
||||
// ephemeral (single-instance Stage-1): a restart drops it, and the DB holds the
|
||||
// durable state — the summary is present, or summarize_requested is still set so
|
||||
// `tapir run` retries. The zero value is ready to use; methods are concurrency-safe.
|
||||
type ProcessingSet struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
// Add marks a key in-flight.
|
||||
func (p *ProcessingSet) Add(key string) { p.m.Store(key, struct{}{}) }
|
||||
|
||||
// Remove clears a key once its summarization finishes (success or failure).
|
||||
func (p *ProcessingSet) Remove(key string) { p.m.Delete(key) }
|
||||
|
||||
// Has reports whether a key is currently in-flight.
|
||||
func (p *ProcessingSet) Has(key string) bool {
|
||||
_, ok := p.m.Load(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
// processingKey scopes the in-flight key by user so one user's summarization is
|
||||
// never confused with another's for the same video id.
|
||||
func processingKey(userID, videoID string) string {
|
||||
return userID + "|" + videoID
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package web
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestProcessingSetAddHasRemove(t *testing.T) {
|
||||
var s ProcessingSet // zero value is usable
|
||||
|
||||
key := processingKey("user-1", "video-1")
|
||||
if s.Has(key) {
|
||||
t.Fatal("fresh set must not report a key as in-flight")
|
||||
}
|
||||
|
||||
s.Add(key)
|
||||
if !s.Has(key) {
|
||||
t.Fatal("Add must mark the key in-flight")
|
||||
}
|
||||
|
||||
// A different user with the same video id is a distinct key.
|
||||
if s.Has(processingKey("user-2", "video-1")) {
|
||||
t.Fatal("keys must be scoped by user")
|
||||
}
|
||||
|
||||
s.Remove(key)
|
||||
if s.Has(key) {
|
||||
t.Fatal("Remove must clear the key")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user