返回 DeepSeek-Reasonix
read_projection.go
根目录 / desktop / internal / workspacestate / read_projection.go
1 package workspacestate
2
3 import (
4 "bytes"
5 "encoding/json"
6 "maps"
7 "path/filepath"
8 "slices"
9 "strings"
10
11 "reasonix/internal/pathidentity"
12 )
13
14 // WorkspaceIndex resolves every persisted workspace root once and reuses the
15 // result for the lifetime of one read projection. Sidebar snapshots commonly
16 // resolve the same roots several times; doing that through ResolveWorkspaceID
17 // would walk every registered root for every sidebar row.
18 type WorkspaceIndex struct {
19 exact map[string]string
20 physical map[string]string
21 resolved map[string]string
22 paths map[string]workspacePathResult
23 }
24
25 type workspacePathResult struct {
26 key string
27 err error
28 }
29
30 func (i *WorkspaceIndex) pathKey(root string) (string, error) {
31 root = strings.TrimSpace(root)
32 if result, ok := i.paths[root]; ok {
33 return result.key, result.err
34 }
35 identity, err := pathidentity.Resolve(root, pathidentity.Options{FollowLeaf: true})
36 i.paths[root] = workspacePathResult{identity.Key, err}
37 return identity.Key, err
38 }
39
40 // SameRoot compares physical roots even before either has a registry owner.
41 // Unknown owners must not compare equal just because both IDs are empty.
42 func (i *WorkspaceIndex) SameRoot(a, b string) bool {
43 left, err := i.pathKey(a)
44 if err != nil || left == "" {
45 return false
46 }
47 right, err := i.pathKey(b)
48 return err == nil && left == right
49 }
50
51 func NewWorkspaceIndex(state State) *WorkspaceIndex {
52 index := &WorkspaceIndex{
53 exact: map[string]string{}, physical: map[string]string{}, resolved: map[string]string{}, paths: map[string]workspacePathResult{},
54 }
55 groups := map[string][]string{}
56 keys := map[string]string{}
57 for id, workspace := range state.Workspaces {
58 root := strings.TrimSpace(workspace.Root)
59 if root == "" {
60 continue
61 }
62 key, err := index.pathKey(root)
63 if err != nil || key == "" {
64 continue
65 }
66 groups[key] = append(groups[key], id)
67 keys[id] = key
68 }
69 for key, ids := range groups {
70 slices.Sort(ids)
71 index.physical[key] = canonicalWorkspaceOwner(state, ids)
72 }
73 for id, workspace := range state.Workspaces {
74 if owner := index.physical[keys[id]]; owner != "" {
75 index.exact[filepath.Clean(strings.TrimSpace(workspace.Root))] = owner
76 }
77 }
78 return index
79 }
80
81 // Resolve returns the persisted owner for root without rescanning registered
82 // roots. Exact persisted spellings are filesystem-free; aliases are resolved
83 // once and cached for the rest of the snapshot.
84 func (i *WorkspaceIndex) Resolve(root string) (string, bool, error) {
85 root = strings.TrimSpace(root)
86 if root == "" {
87 return "", false, nil
88 }
89 clean := filepath.Clean(root)
90 if id := i.exact[clean]; id != "" {
91 return id, true, nil
92 }
93 if id, ok := i.resolved[clean]; ok {
94 return id, id != "", nil
95 }
96 key, err := i.pathKey(root)
97 if err != nil {
98 return "", false, err
99 }
100 id := i.physical[key]
101 i.resolved[clean] = id
102 return id, id != "", nil
103 }
104
105 // FindWorkspace admits the read-only path only for an unambiguous physical
106 // owner. Missing or duplicate roots go through the registration transaction.
107 func FindWorkspace(state State, root string) (string, bool) {
108 if strings.TrimSpace(root) == "" {
109 return "", false
110 }
111 identity, err := pathidentity.Resolve(root, pathidentity.Options{FollowLeaf: true})
112 if err != nil {
113 return "", false
114 }
115 matches := matchingWorkspaceIDs(state, identity)
116 if len(matches) != 1 {
117 return "", false
118 }
119 return matches[0], true
120 }
121
122 // Clone preserves unknown fields while isolating every mutable organization
123 // field. Read-side import planning must never edit a published snapshot.
124 func (o Organization) Clone() Organization {
125 o.Order = slices.Clone(o.Order)
126 o.Imported = maps.Clone(o.Imported)
127 o.extra = cloneUnknownFields(o.extra)
128 o.Groups = slices.Clone(o.Groups)
129 for i := range o.Groups {
130 o.Groups[i].Members = slices.Clone(o.Groups[i].Members)
131 o.Groups[i].extra = cloneUnknownFields(o.Groups[i].extra)
132 }
133 return o
134 }
135
136 func cloneUnknownFields(fields map[string]json.RawMessage) map[string]json.RawMessage {
137 if fields == nil {
138 return nil
139 }
140 clone := make(map[string]json.RawMessage, len(fields))
141 for key, value := range fields {
142 clone[key] = bytes.Clone(value)
143 }
144 return clone
145 }
146
146 lines GO