- Implement atomic state transitions: add EntryService.PatchEntry() with EntryDiff computation and EventEntryTransition event emission. - Add persistent SQLite task queue (JobEngine): add jobs table with crash recovery, exponential backoff retries, concurrency control, and idempotency key deduplication. - Decompose monolithic HTTP handlers: split handlers.go into domain handlers (auth, feeds, entries, jobs, settings, rules, sse). - Implement real-time Server-Sent Events (/api/events/stream) for multi-tab/device state synchronization. - Fix SQLite concurrency: apply busy_timeout(10000) and journal_mode(WAL) via DSN query parameters to resolve SQLITE_BUSY (5). - Fix AI settings & prompt loading: unmarshal JSON from ai_settings, apply configured prompt templates, and enforce strict enabled/API key validations. - Fix undo share state: remove omitempty from ShareUUID to serialize null for state resets. - Fix middle list counter: compute true unread/total stats from DB and clean switcher button labels. - Adapt mobile UI for iOS: add viewport-fit=cover in index.html and calibrate safe-area insets for the bottom navigation and article reader toolbar. - Add architectural documentation and worklog in docs/009_worklog.md.
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/ai"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/events"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/services"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/worker"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *db.DB
|
|
auth *auth.AuthService
|
|
bus *events.EventBus
|
|
entryService *services.EntryService
|
|
feedService *services.FeedService
|
|
jobService *services.JobService
|
|
settingsService *services.SettingsService
|
|
jobEngine *worker.JobEngine
|
|
aiClient *ai.Client
|
|
broadcaster *SSEBroadcaster
|
|
loginLimiter *RateLimiter
|
|
}
|
|
|
|
func NewHandler(
|
|
database *db.DB,
|
|
authService *auth.AuthService,
|
|
bus *events.EventBus,
|
|
entrySvc *services.EntryService,
|
|
feedSvc *services.FeedService,
|
|
jobSvc *services.JobService,
|
|
settingsSvc *services.SettingsService,
|
|
jobEngine *worker.JobEngine,
|
|
) *Handler {
|
|
broadcaster := NewSSEBroadcaster()
|
|
limiter := NewRateLimiter(5, 1*time.Minute)
|
|
|
|
h := &Handler{
|
|
db: database,
|
|
auth: authService,
|
|
bus: bus,
|
|
entryService: entrySvc,
|
|
feedService: feedSvc,
|
|
jobService: jobSvc,
|
|
settingsService: settingsSvc,
|
|
jobEngine: jobEngine,
|
|
aiClient: ai.NewClient(45 * time.Second),
|
|
broadcaster: broadcaster,
|
|
loginLimiter: limiter,
|
|
}
|
|
|
|
// Wire EventBus to SSE Broadcaster
|
|
if bus != nil {
|
|
bus.Subscribe(events.EventJobProgress, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("job_progress", evt.Job)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventJobCompleted, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("job_completed", evt.Job)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventJobFailed, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("job_failed", evt.Job)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventPreferencesSync, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("preferences_updated", evt.DirectAttrs)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventPostCreated, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("post_created", evt.Entry)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventPostUpdated, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("post_updated", evt.Entry)
|
|
return nil
|
|
})
|
|
bus.Subscribe(events.EventVariantUpdated, func(ctx context.Context, evt events.Event) error {
|
|
broadcaster.Broadcast("variant_updated", map[string]interface{}{
|
|
"entry": evt.Entry,
|
|
"variant_key": evt.VariantKey,
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
func (h *Handler) writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func (h *Handler) writeError(w http.ResponseWriter, status int, msg string) {
|
|
h.writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|