package logic import ( "encoding/json" "fmt" "strings" "time" "img_infinite_canvas/internal/domain/design" ) const streamingThinkingThrottle = 450 * time.Millisecond type messageEventState struct { fingerprints map[string]string emittedAt map[string]time.Time } func newMessageEventState() *messageEventState { return &messageEventState{ fingerprints: make(map[string]string), emittedAt: make(map[string]time.Time), } } func (s *messageEventState) shouldEmit(message design.Message, now time.Time) bool { if s == nil { return true } key := messageEventKey(message) fingerprint := messageEventFingerprint(message) if s.fingerprints[key] == fingerprint { return false } if _, seen := s.fingerprints[key]; !seen || messageEventBypassesThrottle(message) { s.record(key, fingerprint, now) return true } if last := s.emittedAt[key]; last.IsZero() || now.Sub(last) >= streamingThinkingThrottle { s.record(key, fingerprint, now) return true } return false } func (s *messageEventState) record(key string, fingerprint string, now time.Time) { s.fingerprints[key] = fingerprint s.emittedAt[key] = now } func messageEventKey(message design.Message) string { if key := strings.TrimSpace(message.ID); key != "" { return key } return fmt.Sprintf("%s:%s:%s:%s:%s", message.ThreadID, message.Role, message.Type, message.Title, message.CreatedAt.UTC().Format(time.RFC3339Nano)) } func messageEventBypassesThrottle(message design.Message) bool { if message.Type == "text_extraction_result" { return true } switch strings.ToLower(strings.TrimSpace(message.Status)) { case "running", "streaming": return false default: return true } } func projectEventFingerprint(project design.Project) string { payload := struct { ID string `json:"id"` UserID string `json:"userId"` Title string `json:"title"` Brief string `json:"brief"` Status design.ProjectStatus `json:"status"` Thumbnail string `json:"thumbnail"` LastThreadID string `json:"lastThreadId"` Viewport design.Viewport `json:"viewport"` Nodes []design.Node `json:"nodes"` Connections []design.Connection `json:"connections"` }{ ID: project.ID, UserID: project.UserID, Title: project.Title, Brief: project.Brief, Status: project.Status, Thumbnail: project.Thumbnail, LastThreadID: project.LastThreadID, Viewport: project.Viewport, Nodes: append([]design.Node(nil), project.Nodes...), Connections: append([]design.Connection(nil), project.Connections...), } data, err := json.Marshal(payload) if err != nil { return fmt.Sprintf("%s:%s:%s:%d:%d", project.ID, project.Status, project.LastThreadID, len(project.Nodes), len(project.Connections)) } return string(data) }