Initial commit: img-infinite-canvas AI design workbench MVP
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>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user