package design import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "io" "strings" "time" ) const CanvasDataPrefix = "SHAKKERDATA://" type CanvasSnapshot struct { ProjectID string `json:"projectId"` Version string `json:"version"` SnapshotID string `json:"snapshotId"` Viewport Viewport `json:"viewport"` Nodes []Node `json:"nodes"` Connections []Connection `json:"connections"` UpdatedAt string `json:"updatedAt"` } func EncodeCanvasSnapshot(project Project) (string, error) { snapshot := CanvasSnapshot{ ProjectID: project.ID, Version: project.Version, SnapshotID: project.SnapshotID, Viewport: project.Viewport, Nodes: append([]Node(nil), project.Nodes...), Connections: append([]Connection(nil), project.Connections...), UpdatedAt: project.UpdatedAt.UTC().Format(time.RFC3339Nano), } payload, err := json.Marshal(snapshot) if err != nil { return "", err } var compressed bytes.Buffer writer := gzip.NewWriter(&compressed) if _, err := writer.Write(payload); err != nil { _ = writer.Close() return "", err } if err := writer.Close(); err != nil { return "", err } return CanvasDataPrefix + base64.StdEncoding.EncodeToString(compressed.Bytes()), nil } func DecodeCanvasSnapshot(canvas string) (CanvasSnapshot, error) { canvas = strings.TrimSpace(canvas) if !strings.HasPrefix(canvas, CanvasDataPrefix) { return CanvasSnapshot{}, fmt.Errorf("%w: canvas snapshot must use %s prefix", ErrInvalidInput, CanvasDataPrefix) } raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(canvas, CanvasDataPrefix)) if err != nil { return CanvasSnapshot{}, err } reader, err := gzip.NewReader(bytes.NewReader(raw)) if err != nil { return CanvasSnapshot{}, err } defer reader.Close() payload, err := io.ReadAll(reader) if err != nil { return CanvasSnapshot{}, err } var snapshot CanvasSnapshot if err := json.Unmarshal(payload, &snapshot); err != nil { return CanvasSnapshot{}, err } return snapshot, nil } func CanvasVersion(now time.Time) string { if now.IsZero() { now = time.Now() } return fmt.Sprintf("%d", now.UnixMilli()) }