44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
3.0 KiB
Go
156 lines
3.0 KiB
Go
package realtime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
)
|
|
|
|
const (
|
|
defaultOutboxInterval = time.Second
|
|
defaultOutboxBatch = 100
|
|
)
|
|
|
|
type OutboxOptions struct {
|
|
DataSource string
|
|
IntervalMillis int
|
|
BatchSize int
|
|
}
|
|
|
|
type OutboxPublisher struct {
|
|
pool *pgxpool.Pool
|
|
bus Bus
|
|
interval time.Duration
|
|
batch int
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func NewOutboxPublisher(ctx context.Context, opts OutboxOptions, bus Bus) (*OutboxPublisher, error) {
|
|
dataSource := strings.TrimSpace(opts.DataSource)
|
|
if dataSource == "" || bus == nil {
|
|
return nil, nil
|
|
}
|
|
pool, err := pgxpool.New(ctx, dataSource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interval := time.Duration(opts.IntervalMillis) * time.Millisecond
|
|
if interval <= 0 {
|
|
interval = defaultOutboxInterval
|
|
}
|
|
batch := opts.BatchSize
|
|
if batch <= 0 {
|
|
batch = defaultOutboxBatch
|
|
}
|
|
runCtx, cancel := context.WithCancel(context.Background())
|
|
return &OutboxPublisher{
|
|
pool: pool,
|
|
bus: bus,
|
|
interval: interval,
|
|
batch: batch,
|
|
ctx: runCtx,
|
|
cancel: cancel,
|
|
}, nil
|
|
}
|
|
|
|
func (p *OutboxPublisher) Start() error {
|
|
if p == nil || p.pool == nil {
|
|
return nil
|
|
}
|
|
p.wg.Add(1)
|
|
go func() {
|
|
defer p.wg.Done()
|
|
p.loop()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (p *OutboxPublisher) Run() error {
|
|
if p == nil || p.pool == nil {
|
|
return nil
|
|
}
|
|
p.loop()
|
|
return nil
|
|
}
|
|
|
|
func (p *OutboxPublisher) Shutdown() {
|
|
if p == nil {
|
|
return
|
|
}
|
|
if p.cancel != nil {
|
|
p.cancel()
|
|
}
|
|
p.wg.Wait()
|
|
if p.pool != nil {
|
|
p.pool.Close()
|
|
}
|
|
}
|
|
|
|
func (p *OutboxPublisher) loop() {
|
|
ticker := time.NewTicker(p.interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
if err := p.publishPending(p.ctx); err != nil && p.ctx.Err() == nil {
|
|
logx.Errorf("publish outbox events failed: %v", err)
|
|
}
|
|
select {
|
|
case <-p.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *OutboxPublisher) publishPending(ctx context.Context) error {
|
|
tx, err := p.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
rows, err := tx.Query(ctx, `
|
|
SELECT id, user_id, aggregate_id, event_type, thread_id, payload_json, created_at
|
|
FROM outbox_events
|
|
WHERE status = 'pending'
|
|
ORDER BY created_at ASC
|
|
LIMIT $1
|
|
FOR UPDATE SKIP LOCKED
|
|
`, p.batch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
events, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Event, error) {
|
|
var event Event
|
|
var payload []byte
|
|
if err := row.Scan(&event.ID, &event.UserID, &event.ProjectID, &event.Type, &event.ThreadID, &payload, &event.CreatedAt); err != nil {
|
|
return Event{}, err
|
|
}
|
|
event.Payload = json.RawMessage(payload)
|
|
return event, nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, event := range events {
|
|
if err := p.bus.Publish(ctx, event); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE outbox_events
|
|
SET status = 'published', published_at = NOW()
|
|
WHERE id = $1
|
|
`, event.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|