| 1 | package store |
| 2 | |
| 3 | import ( |
| 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. |
| 23 | type 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. |
| 46 | type 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". |
| 64 | func (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. |
| 97 | func (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. |
| 144 | func (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. |
| 181 | func (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 | |