feat(canvas): 条件读取与历史 - 减少画布同步传输并校准版本
This commit is contained in:
1 parent
bcccddb4e9
commit
6ad1adc197
17 files changed
+783
-25
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -169,19 +170,99 @@ func buildCanvasSnapshot(before *model.CanvasProject, after model.CanvasProject,
|
||||
|
||||
// Row metadata is authoritative, including changes made by project association operations.
|
||||
func canvasProjectPayload(project model.CanvasProject) (json.RawMessage, error) {
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(project.PayloadJSON), &payload); err != nil {
|
||||
return nil, err
|
||||
metadata := []struct {
|
||||
key string
|
||||
value any
|
||||
}{
|
||||
{key: "id", value: project.ID},
|
||||
{key: "title", value: project.Title},
|
||||
{key: "projectId", value: project.ProjectID},
|
||||
{key: "revision", value: project.Revision},
|
||||
{key: "createdAt", value: project.CreatedAt},
|
||||
{key: "updatedAt", value: project.UpdatedAt},
|
||||
}
|
||||
if payload == nil {
|
||||
return nil, kernel.BadAuthRequest("画布数据格式错误")
|
||||
}
|
||||
for key, value := range map[string]any{"id": project.ID, "title": project.Title, "projectId": project.ProjectID, "revision": project.Revision, "createdAt": project.CreatedAt, "updatedAt": project.UpdatedAt} {
|
||||
encoded, err := json.Marshal(value)
|
||||
replacements := make(map[string]json.RawMessage, len(metadata))
|
||||
for _, item := range metadata {
|
||||
encoded, err := json.Marshal(item.value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload[key] = encoded
|
||||
replacements[item.key] = encoded
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(project.PayloadJSON))
|
||||
opening, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opening != json.Delim('{') {
|
||||
return nil, kernel.BadAuthRequest("画布数据格式错误")
|
||||
}
|
||||
|
||||
result := make([]byte, 0, len(project.PayloadJSON)+128)
|
||||
result = append(result, '{')
|
||||
written := make(map[string]struct{}, len(replacements))
|
||||
first := true
|
||||
appendField := func(key string, value json.RawMessage) error {
|
||||
if !first {
|
||||
result = append(result, ',')
|
||||
}
|
||||
first = false
|
||||
encodedKey, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = append(result, encodedKey...)
|
||||
result = append(result, ':')
|
||||
result = append(result, value...)
|
||||
return nil
|
||||
}
|
||||
for decoder.More() {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := token.(string)
|
||||
if !ok {
|
||||
return nil, kernel.BadAuthRequest("画布数据格式错误")
|
||||
}
|
||||
var value json.RawMessage
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if replacement, exists := replacements[key]; exists {
|
||||
if _, duplicate := written[key]; duplicate {
|
||||
continue
|
||||
}
|
||||
value = replacement
|
||||
written[key] = struct{}{}
|
||||
}
|
||||
if err := appendField(key, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
closing, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if closing != json.Delim('}') {
|
||||
return nil, kernel.BadAuthRequest("画布数据格式错误")
|
||||
}
|
||||
var trailing json.RawMessage
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return nil, kernel.BadAuthRequest("画布数据格式错误")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range metadata {
|
||||
if _, exists := written[item.key]; exists {
|
||||
continue
|
||||
}
|
||||
if err := appendField(item.key, replacements[item.key]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result = append(result, '}')
|
||||
return json.RawMessage(result), nil
|
||||
}
|
||||
@@ -4,13 +4,55 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestCanvasProjectPayloadReplacesMetadataAndPreservesDocument(t *testing.T) {
|
||||
project := model.CanvasProject{
|
||||
ID: "canvas-authoritative",
|
||||
Title: "Current title",
|
||||
ProjectID: "project-current",
|
||||
Revision: 9,
|
||||
CreatedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2026, 2, 3, 4, 5, 6, 0, time.UTC),
|
||||
PayloadJSON: `{"id":"stale","title":"stale","revision":1,"nodes":[ { "id": "node-1", "metadata": {"content":"large"} } ],"connections":[],"custom":{"preserve":true}}`,
|
||||
}
|
||||
raw, err := canvasProjectPayload(project)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var actual map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &actual); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for key, want := range map[string]any{
|
||||
"id": project.ID, "title": project.Title, "projectId": project.ProjectID,
|
||||
"revision": project.Revision, "createdAt": project.CreatedAt, "updatedAt": project.UpdatedAt,
|
||||
} {
|
||||
var got any
|
||||
if err := json.Unmarshal(actual[key], &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
encodedWant, _ := json.Marshal(want)
|
||||
var decodedWant any
|
||||
_ = json.Unmarshal(encodedWant, &decodedWant)
|
||||
if !reflect.DeepEqual(got, decodedWant) {
|
||||
t.Errorf("%s = %#v, want %#v", key, got, decodedWant)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"nodes", "connections", "custom"} {
|
||||
if len(actual[key]) == 0 || !json.Valid(actual[key]) {
|
||||
t.Errorf("original %s payload was not preserved: %s", key, actual[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanvasHistoryRestore(t *testing.T) {
|
||||
svc := newCanvasHistoryTestService(t)
|
||||
actor := &model.User{ID: "owner"}
|
||||
|
||||
@@ -218,6 +218,10 @@ func (s *Service) UserCanvasProjectSummaries(userID string) ([]UserDataSummary,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProjectMetadata(userID string, id string) (*model.CanvasProject, error) {
|
||||
return s.repo.CanvasProjectMetadataForUser(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProject(userID string, id string) (json.RawMessage, error) {
|
||||
project, err := s.repo.CanvasProjectForUser(userID, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user