oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package store
2
3import (
4 "fmt"
5
6 "amadan.net/oficina/sheets/internal/sheets/calc"
7)
8
9// ChangedCell is one cell a write altered.
10//
11// Raw is the cell's input text: what an editor should show when you
12// select it. Value is what it computes to: what a grid should paint.
13// Keeping both is the whole point — a client that only had Value would
14// replace "=A1*2" with "42" in the editor the moment somebody else's
15// edit arrived.
16//
17// Derived distinguishes the cell that was typed into from the cells that
18// changed because of it. Spec §7: derived cells carry the actor of the
19// edit that caused them, not the owner of the formula, and a derived
20// flag — so version history can say "your edit changed 400 cells" rather
21// than crediting a formula's output to whoever typed a number into a
22// different cell.
23type ChangedCell struct {
24 RowID, ColID uint64
25 Raw string
26 Value calc.Value
27 Derived bool
28}
29
30// Report is what one write changed.
31//
32// Version is the single number a client orders paints by (spec §7:
33// every message carries the cell's version, and clients discard
34// out-of-order paints by version rather than by arrival). Every cell in
35// one report shares it, because they were all written by one Set and one
36// recalculation pass — a per-cell version would be the same number
37// repeated once per cell.
38//
39// Capped is true when the edit's dependent set was wider than
40// calc.MaxDirtyPerPass, so no recalculation ran and Cells covers the
41// edited cell alone. Every dependent is still durably marked dirty and
42// will be recomputed by the next Open — nothing is lost — but a live
43// client must not paint the partial truth. It resyncs instead. This is
44// the honest error the cap exists to produce, surfaced rather than
45// swallowed.
46type Report struct {
47 Version uint64
48 Cells []ChangedCell
49 Capped bool
50 // Stamps is how many versions this write consumed: one per typed
51 // cell, ending at Version. A client that last saw version H and
52 // receives this report missed nothing iff Version <= H + Stamps.
53 Stamps int
54}
55
56// Version is the version a snapshot taken right now would carry.
57//
58// It is the handle's next stamp, not the last one issued, and that is
59// the point: a window fetched at Version N already contains every edit
60// stamped below N, so a live message arriving late with a version below
61// N is one the snapshot has already applied and the client is right to
62// discard it. Per-cell versions would be more precise and buy nothing —
63// the client's question is only ever "is this older than what I have".
64func (h *Handle) Version() uint64 {
65 h.mu.Lock()
66 defer h.mu.Unlock()
67 return h.nextVersion
68}
69
70// SetReport writes a cell's new raw input, recalculates forward from it,
71// and reports every cell that changed.
72//
73// The write path is unchanged from what Set has always done, and the
74// crash-safety reasoning is the load-bearing part of it:
75//
76// For an edit to a surviving cell, the input write — plus the version
77// bump that IS the dirty marker (models.go: CacheVersion records the
78// InputVersion its cache was derived from, so bumping InputVersion
79// without yet touching CacheVersion is exactly what makes a cell look
80// dirty) — lands in one transaction. Recalculation, and persisting what
81// it computes, happens afterward in a second. A crash between the two
82// leaves precisely the state Open's crash-recovery pass exists to detect
83// and fix: no separate marker column is needed because the version gap
84// IS the marker, and clearing it is writing a CacheVersion that has
85// caught back up.
86//
87// Blanking a cell cannot use that mechanism on the cell itself — its row
88// is deleted, so there is no surviving row to carry a marker — so it
89// marks its DEPENDENTS dirty instead, atomically with the delete. See
90// setBlank and TestBlankCrashLeavesDependentsRecoverable.
91//
92// What is new is the report, and it is built AFTER both transactions
93// commit, from the graph's now-current cache. That ordering matters: a
94// report assembled before the commit would tell a live client about an
95// edit that a failed transaction then rolled back, and the client has no
96// way to un-paint it.
97func (h *Handle) SetReport(rowID, colID uint64, raw string, actor uint64) (Report, error) {
98 h.mu.Lock()
99 defer h.mu.Unlock()
100
101 key, ok := h.cellKey(rowID, colID)
102 if !ok {
103 return Report{}, fmt.Errorf("store: row %d col %d is not part of tab %d",
104 rowID, colID, h.tabID)
105 }
106
107 node, blank, err := contentNode(raw)
108 if err != nil {
109 return Report{}, err
110 }
111
112 if h.nextVersion >= maxContentVersion {
113 return Report{}, errVersionExhausted
114 }
115 v := h.nextVersion
116 prevRaw := h.rawText[key]
117
118 var (
119 touched map[calc.CellKey]struct{}
120 capped bool
121 )
122 if blank {
123 touched, capped, err = h.setBlank(rowID, colID, key, v, actor, prevRaw)
124 } else {
125 touched, capped, err = h.setContent(rowID, colID, key, node, raw, v, actor, prevRaw)
126 }
127 if err != nil {
128 return Report{}, err
129 }
130 rep := h.report(key, rowID, colID, v, touched, capped)
131 rep.Stamps = 1
132 return rep, nil
133}
134
135// report turns a recomputed touched set into the cells a client paints.
136//
137// The edited cell is always present, whatever the touched set holds.
138// setBlank's touched set is its DEPENDENTS only — the origin's row is
139// gone, so it is not in the graph to be recomputed — and without this
140// the other tab would never clear the cell you just emptied, which is
141// the most visible bug this whole layer could have.
142//
143// Caller must hold h.mu.
144func (h *Handle) report(origin calc.CellKey, rowID, colID, v uint64,
145 touched map[calc.CellKey]struct{}, capped bool) Report {
146
147 rep := Report{Version: v, Capped: capped, Cells: make([]ChangedCell, 0, len(touched)+1)}
148
149 rep.Cells = append(rep.Cells, ChangedCell{
150 RowID: rowID, ColID: colID,
151 Raw: h.rawText[origin], // absent from the map once blanked, which yields ""
152 Value: h.originValue(origin, capped),
153 })
154
155 for k := range touched {
156 if k == origin {
157 continue // already appended, and never as derived
158 }
159 ids, ok := h.keyToIDs[k]
160 if !ok {
161 continue // defensive: every touched key was registered via SetFormula
162 }
163 val, _ := h.graph.CachedValue(k)
164 rep.Cells = append(rep.Cells, ChangedCell{
165 RowID: ids.RowID, ColID: ids.ColID,
166 Raw: h.rawText[k],
167 Value: val,
168 Derived: true,
169 })
170 }
171 return rep
172}
173
174// originValue reads the edited cell's current value.
175//
176// A capped write never ran Recalc, so the graph's cache for the origin
177// is whatever it held before — stale, and wrong to send. Report an empty
178// value instead and let Capped tell the client to resync; a stale number
179// painted as fresh is worse than a blank the client immediately
180// replaces.
181func (h *Handle) originValue(origin calc.CellKey, capped bool) calc.Value {
182 if capped {
183 return calc.Value{}
184 }
185 v, _ := h.graph.CachedValue(origin)
186 return v
187}
188