Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_canvas_events.go
2026-09-14 01:11:56 +08:00

82 lines
2.8 KiB
Go

package app
import (
"reflect"
"infinite-canvas/backend/internal/repository"
)
// Persist the delta in the same transaction as the canvas and run checkpoint.
// Clients can project it without fetching the entire canvas for every task.
func cloudAgentCanvasEventRecorder(runID string, state *cloudAgentRuntime) cloudAgentMutationRecorder {
return func(repo *repository.Repository, input cloudAgentMutationInput) error {
if err := cloudAgentMutationRecorderForRun(runID)(repo, input); err != nil {
return err
}
return emitCloudAgentCanvasChange(repo, runID, state, input.UserID, input.CanvasID, input.BeforeJSON, input.Operation)
}
}
func cloudAgentObjectChanges(before, after any) []map[string]any {
previous := map[string]map[string]any{}
for _, item := range creationMaps(before) {
previous[stringValue(item["id"])] = item
}
changes := []map[string]any{}
for _, item := range creationMaps(after) {
old := previous[stringValue(item["id"])]
if !reflect.DeepEqual(old, item) {
changes = append(changes, map[string]any{"before": old, "after": item})
}
}
return changes
}
func emitCloudAgentCanvasChange(repo *repository.Repository, runID string, state *cloudAgentRuntime, userID, canvasID, beforeJSON, operation string) error {
before, err := creationDocument(beforeJSON)
if err != nil {
return err
}
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
if err != nil {
return err
}
after, err := creationDocument(canvas.PayloadJSON)
if err != nil {
return err
}
nodes := cloudAgentObjectChanges(before["nodes"], after["nodes"])
edges := cloudAgentObjectChanges(before["connections"], after["connections"])
if len(nodes) == 0 && len(edges) == 0 {
return nil
}
actions := []map[string]any{}
for _, change := range nodes {
node := change["after"].(map[string]any)
action := "updated"
if old, _ := change["before"].(map[string]any); old == nil {
action = "created"
}
actions = append(actions, map[string]any{"action": action, "nodeId": node["id"], "title": node["title"], "nodeType": node["type"]})
}
byID, err := creationObjects(after["nodes"])
if err != nil {
return err
}
for _, change := range edges {
edge := change["after"].(map[string]any)
if node := byID[stringValue(edge["fromNodeId"])]; node != nil {
actions = append(actions, map[string]any{"action": "referenced", "nodeId": node["id"], "title": node["title"], "nodeType": node["type"], "targetNodeId": edge["toNodeId"]})
}
}
payload := map[string]any{
"canvasId": canvasID, "operation": operation, "actions": actions,
"canvasPatch": map[string]any{"canvasId": canvasID, "updatedAt": after["updatedAt"], "nodes": nodes, "connections": edges},
}
if state.CallIndex >= 0 && state.CallIndex < len(state.Calls) {
payload["callId"] = state.Calls[state.CallIndex].ID
}
state.event(runID, "canvas_updated", payload)
return nil
}