Repository navigation
Expand file tree
/
Copy pathchatv3_host_teams.go
More file actions
323 lines (299 loc) · 11.9 KB
/
Copy pathchatv3_host_teams.go
File metadata and controls
323 lines (299 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package main
import (
"errors"
"sync"
"github.com/Agent-Field/codeaf/internal/remote"
teamstore "github.com/Agent-Field/codeaf/internal/teams"
"github.com/Agent-Field/codeaf/internal/tui3"
)
// hostTeams is [tui3.TeamsSeam] over the wire: the ENGINE machine's teams file
// and Traffic logs, which are where the far session's team tools keep them
// (internal/remote's wire_teams.go).
//
// IT IS BUILT LIKE [hostWorld], for the one call the surface makes on its
// loop. [tui3.TeamsSeam.Load] is asked at an opening and must not block, so it
// answers what is held and nothing else; when nothing is held yet it says so,
// and the surface asks [tui3.TeamsSeam.ReadSince] off its loop, once, which
// fills what is held. Every other door here is called from a command and may
// take a round trip: the Traffic clock's turn, and the write an edit queued.
// There is no clock of its own: the surface asks again through its Traffic
// clock and its read-only teams refresh clock, even with no managed chat open.
//
// NOTHING HERE READS THIS LAPTOP'S PROFILE. The teams a window over --host
// draws are the far machine's, or none: an engine without the doors gets no
// seam at all ([hostTeamsSeam]), and the surface turns teams off rather than
// falling back to the file on this disk, which the far session never reads.
type hostTeams struct {
// read, write and traffic are the three wire doors, as closures for
// [hostWorld]'s reason: a test hands them a conflict without a pipe.
read func(stamp string, reserved []float64) (remote.TeamsReading, error)
write func(base string, teams []teamstore.Team) (remote.TeamsReading, error)
traffic func(team, after string, limit int) (remote.TeamsTraffic, error)
history func(team, after, version string, limit int) (remote.TeamsTraffic, error)
// The delegation doors, nil when the engine does not answer them
// ([remote.Welcome.Delegation]); the seam then hands the surface none.
defaults func() (teamstore.Defaults, error)
// applyDefault writes one `teams.` row ([remote.Welcome.TeamSettings]).
// Nil leaves the settings Teams tab read-only.
applyDefault func(key, raw string) (teamstore.Defaults, error)
packets func(scope, stamp string) (remote.PacketsReading, error)
raise func(p teamstore.Packet) (teamstore.Packet, error)
decide func(id, by, decision, reason string) (teamstore.Packet, error)
escalate func(id, by, to, reason string) (teamstore.Packet, error)
spend func(team, day, stamp string) (remote.SpendReading, error)
deleteOne func(team string) (remote.DeleteTeamReply, error)
deleteChecked func(team string, expected []string) (remote.DeleteTeamReply, error)
// The wrap-up's two doors, nil when the engine does not answer them
// ([remote.Welcome.WrapUp]).
wrapUp func(team, text string) error
acceptClosing func(id string) (remote.AcceptClosingReply, error)
mu sync.Mutex
// teams and stamp are the last list the engine answered with and the
// file's stamp it was at; known says there has been one.
teams []teamstore.Team
stamp string
known bool
// reserved is the palette's reserved hues as the surface last passed
// them, for a read this seam makes on its own.
reserved []float64
}
// hostTeamsTries is how many times a write that met another writer is made
// again from a fresh read before it gives up and says so.
const hostTeamsTries = 3
// errHostTeamsBusy is a write that met another writer on every try.
var errHostTeamsBusy = errors.New("the teams on the far machine kept changing while this was written; try again")
func newHostTeams(far hostFar, delegation, wrapUp, settings bool) *hostTeams {
h := &hostTeams{
read: far.client.TeamsRead,
write: far.client.TeamsUpdate,
traffic: far.client.TeamsTraffic,
history: far.client.TeamsHistory,
}
if delegation {
c := far.client
if c.Welcome().ConversationDelete {
h.deleteChecked = func(team string, expected []string) (remote.DeleteTeamReply, error) {
return c.TeamsDelete(team, expected)
}
}
h.defaults, h.packets, h.raise = c.TeamsDefaults, c.TeamsPackets, c.TeamsRaise
h.decide, h.escalate, h.spend, h.deleteOne = c.TeamsDecide, c.TeamsEscalate, c.TeamsSpend, func(team string) (remote.DeleteTeamReply, error) { return c.TeamsDelete(team) }
if wrapUp {
h.wrapUp, h.acceptClosing = c.TeamsWrapUp, c.TeamsAcceptClosing
}
}
if settings && far.client != nil {
h.applyDefault = far.client.TeamsApplyDefault
}
return h
}
// hostTeamsSeam is the seam the --host door hands the surface: the far
// machine's teams when the engine answers the teams doors, and the zero seam
// when it does not or there is no connection. The zero seam over --host is
// teams off (internal/tui3's [app.teamsOff]), never this laptop's file.
func hostTeamsSeam(far hostFar, welcome remote.Welcome) tui3.TeamsSeam {
if far.client == nil || !welcome.Teams {
return tui3.TeamsSeam{}
}
if welcome.TeamMembershipVersion != remote.TeamMembershipVersion {
return tui3.TeamsSeam{}
}
return newHostTeams(far, welcome.Delegation, welcome.WrapUp, welcome.TeamSettings).seam()
}
// seam is h as the surface's functions. The delegation doors are handed only
// when the engine answers them, so an older engine's window has a seam whose
// delegation doors are nil, which the surface says rather than guesses at.
func (h *hostTeams) seam() tui3.TeamsSeam {
s := tui3.TeamsSeam{Sharing: h.sharing, Load: h.load, ReadSince: h.readSince, Update: h.update, Traffic: h.readTraffic}
if h.history != nil {
s.DisplayTraffic = func(team, after, version string, limit int) ([]teamstore.Entry, string, string, bool, error) {
got, err := h.history(team, after, version, limit)
return got.Entries, got.Cursor, got.Mirror, got.Reset, err
}
}
if h.applyDefault != nil {
s.ApplyDefault = h.applyDefault
}
if h.defaults == nil {
return s
}
s.Defaults, s.Raise, s.Decide, s.Escalate = h.defaults, h.raise, h.decide, h.escalate
s.Packets, s.Spend, s.Delete = h.readPackets, h.readSpend, h.forget
if h.deleteChecked != nil {
s.DeleteChecked = h.forgetChecked
}
if h.wrapUp != nil && h.acceptClosing != nil {
s.WrapUp, s.AcceptClosing = h.wrapUp, h.closeOnReport
}
return s
}
// closeOnReport is [tui3.TeamsSeam.AcceptClosing]: the engine closes the team
// on its report, and the list held here is read again, because the file moved.
func (h *hostTeams) closeOnReport(id string) (bool, error) {
reply, err := h.acceptClosing(id)
if err != nil {
return false, err
}
if reply.Closed {
if _, _, _, err := h.readSince("", h.reservedHues()); err != nil {
return true, err
}
}
return reply.Closed, nil
}
// readPackets is [tui3.TeamsSeam.Packets]: one round trip, a few bytes when
// the engine's packet files have not moved.
func (h *hostTeams) readPackets(scope, since string) ([]teamstore.Packet, string, bool, error) {
got, err := h.packets(scope, since)
if err != nil {
return nil, "", false, err
}
return got.Packets, got.Stamp, got.Same, nil
}
// readSpend is [tui3.TeamsSeam.Spend]: one round trip, a few bytes when
// neither the engine's teams file nor its ledger moved.
func (h *hostTeams) readSpend(team, day, since string) (teamstore.Spend, string, bool, error) {
got, err := h.spend(team, day, since)
if err != nil {
return teamstore.Spend{}, "", false, err
}
if got.Same {
return teamstore.Spend{}, got.Stamp, true, nil
}
if got.Spend == nil {
return teamstore.Spend{Team: team, Day: day}, got.Stamp, false, nil
}
return *got.Spend, got.Stamp, false, nil
}
// forget is [tui3.TeamsSeam.Delete]: the engine forgets the team and its
// files, and the list held here is read again, because the file moved.
func (h *hostTeams) forget(team string) ([]string, error) {
reply, err := h.deleteOne(team)
if err != nil {
return nil, err
}
if _, _, _, err := h.readSince("", h.reservedHues()); err != nil {
return reply.Gone, err
}
return reply.Gone, nil
}
// load is [tui3.TeamsSeam.Load]: what is held, now, and never the wire.
func (h *hostTeams) load(reserved []float64) ([]teamstore.Team, string, bool) {
return h.held(reserved)
}
// held is the list held, a copy, and remembers the reserved hues asked with.
func (h *hostTeams) held(reserved []float64) ([]teamstore.Team, string, bool) {
h.mu.Lock()
defer h.mu.Unlock()
if reserved != nil {
h.reserved = append([]float64(nil), reserved...)
}
return cloneTeams(h.teams), h.stamp, h.known
}
// keep holds a list the engine answered with.
func (h *hostTeams) keep(teams []teamstore.Team, stamp string) {
h.mu.Lock()
defer h.mu.Unlock()
h.teams, h.stamp, h.known = cloneTeams(teams), stamp, true
}
// readSince is [tui3.TeamsSeam.ReadSince]: one round trip, whose answer is a
// few bytes when the file is still at since.
func (h *hostTeams) readSince(since string, reserved []float64) ([]teamstore.Team, string, bool, error) {
reading, err := h.read(since, reserved)
if err != nil {
return nil, "", false, err
}
if reading.Same {
return nil, reading.Stamp, true, nil
}
h.keep(reading.Teams, reading.Stamp)
return reading.Teams, reading.Stamp, false, nil
}
// update is [tui3.TeamsSeam.Update] as a compare-and-swap over the wire.
//
// THE LOCK CANNOT CROSS THE WIRE, SO THE WRITE IS CONDITIONAL. The change is
// made to the list held (or read now, when none is), and the whole list goes
// back with the stamp it was made from; the engine writes it only if its file
// is still at that stamp (internal/teams' ChangeIf). A file that moved in
// between (the far session made a manager, set a handle, started a member)
// answers Stale, and the list is read again and the change made again on top
// of it, up to [hostTeamsTries] times. So what another writer did is kept, as
// the local store's read-modify-write keeps it, and nothing is ever written
// over a list this window did not see.
func (h *hostTeams) update(change func(*teamstore.File) error) ([]teamstore.Team, string, error) {
teams, stamp, known := h.held(nil)
for try := 0; try < hostTeamsTries; try++ {
if !known || try > 0 {
reserved := h.reservedHues()
fresh, at, _, err := h.readSince("", reserved)
if err != nil {
return nil, "", err
}
teams, stamp = fresh, at
}
f := &teamstore.File{Version: teamstore.Version, Teams: cloneTeams(teams)}
if err := change(f); err != nil {
return nil, "", err
}
reply, err := h.write(stamp, f.Teams)
if err != nil {
return nil, "", err
}
if reply.Stale {
continue
}
h.keep(reply.Teams, reply.Stamp)
return reply.Teams, reply.Stamp, nil
}
return nil, "", errHostTeamsBusy
}
// reservedHues is the palette's reserved hues as last passed.
func (h *hostTeams) reservedHues() []float64 {
h.mu.Lock()
defer h.mu.Unlock()
return append([]float64(nil), h.reserved...)
}
// readTraffic is [tui3.TeamsSeam.Traffic]: one round trip, answered from a
// stat on the engine when the log has not moved.
func (h *hostTeams) readTraffic(team, after string, limit int) ([]teamstore.Entry, error) {
got, err := h.traffic(team, after, limit)
if err != nil {
return nil, err
}
return got.Entries, nil
}
// cloneTeams is a copy of teams that shares nothing with it.
func cloneTeams(teams []teamstore.Team) []teamstore.Team {
if teams == nil {
return nil
}
out := make([]teamstore.Team, len(teams))
for i, t := range teams {
out[i] = t.Clone()
}
return out
}
func (h *hostTeams) forgetChecked(team string, expected []string) ([]string, error) {
reply, err := h.deleteChecked(team, expected)
if err != nil {
return nil, err
}
if _, _, _, err := h.readSince("", h.reservedHues()); err != nil {
return reply.Gone, err
}
return reply.Gone, nil
}
// sharing asks the engine rather than borrowing the viewing computer's pairing.
func (h *hostTeams) sharing() (bool, error) {
_, stamp, _ := h.held(nil)
reading, err := h.read(stamp, h.reservedHues())
if err != nil {
return false, err
}
if reading.Sharing != nil {
return *reading.Sharing, nil
}
// Older engines gate their own history. Keep that display door available
// when they cannot report the gate separately.
return h.history != nil, nil
}