| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | |
| 6 | "amadan.net/oficina/sheets/internal/sheets/calc" |
| 7 | "amadan.net/oficina/sheets/internal/sheets/format" |
| 8 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 9 | ) |
| 10 | |
| 11 | // gridCell is one cell on the wire. |
| 12 | // |
| 13 | // R and C are grid POSITIONS, not stable ids. The client thinks in |
| 14 | // coordinates — it is painting a grid — and stable ids are a storage |
| 15 | // concern it has no use for. The translation happens here, in one |
| 16 | // place, rather than leaking row and column ids into JavaScript where |
| 17 | // they would have to be kept in sync with every insert. |
| 18 | // |
| 19 | // Raw and Display are both sent, always. A client with only Display |
| 20 | // replaces "=A1*2" with "42" in the editor the moment somebody else's |
| 21 | // edit arrives; a client with only Raw has to evaluate formulas itself, |
| 22 | // which is the one thing this design will not have it do. |
| 23 | type gridCell struct { |
| 24 | R int `json:"r"` |
| 25 | C int `json:"c"` |
| 26 | Raw string `json:"raw"` |
| 27 | Display string `json:"display"` |
| 28 | Derived bool `json:"derived,omitempty"` |
| 29 | // Num marks a cell whose COMPUTED value is a number, so the client |
| 30 | // can right-align it. |
| 31 | // |
| 32 | // It is a property of the value, not of the input: "=1+1" is a |
| 33 | // number and "1a" is text, and only the engine knows which. The |
| 34 | // alternative — letting the client sniff the display string — puts |
| 35 | // a second, worse number parser in JavaScript, and it would |
| 36 | // disagree with this one the first time a number format put a |
| 37 | // currency symbol in front of the digits. |
| 38 | Num bool `json:"n,omitempty"` |
| 39 | // F is the id of this cell's interned format, into the response's |
| 40 | // own Formats table. Empty for an unformatted cell — the common |
| 41 | // case, and the reason this is a key rather than an inline object. |
| 42 | F string `json:"f,omitempty"` |
| 43 | } |
| 44 | |
| 45 | // gridResponse is what both GET and POST answer, and what the SSE |
| 46 | // stream carries. One shape, so the client has one function that |
| 47 | // applies a batch of cells and does not care where it came from. |
| 48 | // |
| 49 | // Version orders paints (spec §7). Resync tells a client that this |
| 50 | // response is not the whole truth and it should refetch its window — |
| 51 | // set when a write's dependent set was too wide to recalculate |
| 52 | // synchronously. |
| 53 | // |
| 54 | // Client is the id the poster sent, echoed back so a tab can skip its |
| 55 | // own edit arriving over the stream. Without it, the tab you typed in |
| 56 | // repaints the cell you are still editing. |
| 57 | type gridResponse struct { |
| 58 | Version uint64 `json:"version"` |
| 59 | // omitempty: a write response describes changed cells, not a |
| 60 | // window, and "rows":0,"cols":0 on every edit is a claim about a |
| 61 | // window nobody asked for. |
| 62 | Rows int `json:"rows,omitempty"` |
| 63 | Cols int `json:"cols,omitempty"` |
| 64 | Cells []gridCell `json:"cells"` |
| 65 | Resync bool `json:"resync,omitempty"` |
| 66 | // Stamps is how many versions the write behind this response |
| 67 | // consumed (store.Report.Stamps): 1 for a cell, N for an N-cell |
| 68 | // batch, absent for format, size and snapshot responses, which |
| 69 | // consume none. A client holding version H missed an edit iff |
| 70 | // Version > H + max(Stamps, 1); the one of slack is because those |
| 71 | // stampless responses report the NEXT stamp (Handle.Version), not |
| 72 | // the last one issued. Native's SheetModel.accept is the reader; |
| 73 | // grid.js only drops older payloads and ignores it. |
| 74 | Stamps int `json:"stamps,omitempty"` |
| 75 | Client string `json:"client,omitempty"` |
| 76 | // Formats is every distinct style the cells above refer to, keyed |
| 77 | // by the id each cell's F names. Absent when nothing in the batch |
| 78 | // is formatted. |
| 79 | // |
| 80 | // A batch carries only the formats IT mentions, not the tab's whole |
| 81 | // table, so a client must merge rather than replace — see grid.js. |
| 82 | // Replacing would drop every style outside the edited cell the |
| 83 | // first time somebody typed into an imported sheet. |
| 84 | Formats styleTable `json:"formats,omitempty"` |
| 85 | // Sizes is the tab's column widths and row heights, sent whole |
| 86 | // rather than as a delta — see sizesResponse. Absent on a batch |
| 87 | // that is not about layout, which is almost all of them. |
| 88 | Sizes *wireSizes `json:"sizes,omitempty"` |
| 89 | } |
| 90 | |
| 91 | // display turns a computed value into the string the grid paints. |
| 92 | // |
| 93 | // The server owns every string on screen. That is spec §12's gate — at |
| 94 | // least one check crossing from computed value to painted output — and |
| 95 | // it is also what keeps the client from needing a number-format |
| 96 | // implementation that could disagree with the exporter's. |
| 97 | // isNumber reports whether a computed value should read as a number. |
| 98 | // |
| 99 | // Booleans are excluded deliberately: Excel centres TRUE and FALSE, and |
| 100 | // they are not quantities. Errors are excluded for the same reason. |
| 101 | func isNumber(v calc.Value) bool { return v.Kind == calc.KindNum } |
| 102 | |
| 103 | func display(v calc.Value, f *store.Format) string { |
| 104 | numFmt := "" |
| 105 | if f != nil { |
| 106 | numFmt = f.NumberFmt |
| 107 | } |
| 108 | return format.Display(v, numFmt) |
| 109 | } |
| 110 | |
| 111 | // posIndex maps stable ids to grid positions, both ways round. |
| 112 | // |
| 113 | // Built once per request from the handle's own row and column records. |
| 114 | // A map rather than a scan because a 100×26 window asks 2,600 times and |
| 115 | // a linear scan per cell turns an O(n) render into O(n²). |
| 116 | type posIndex struct { |
| 117 | rowPos map[uint64]int |
| 118 | colPos map[uint64]int |
| 119 | rowID map[int]uint64 |
| 120 | colID map[int]uint64 |
| 121 | } |
| 122 | |
| 123 | func newPosIndex(h *store.Handle) posIndex { |
| 124 | idx := posIndex{ |
| 125 | rowPos: map[uint64]int{}, colPos: map[uint64]int{}, |
| 126 | rowID: map[int]uint64{}, colID: map[int]uint64{}, |
| 127 | } |
| 128 | for _, r := range h.GridRows() { |
| 129 | idx.rowPos[r.ID] = r.Pos |
| 130 | idx.rowID[r.Pos] = r.ID |
| 131 | } |
| 132 | for _, c := range h.GridCols() { |
| 133 | idx.colPos[c.ID] = c.Pos |
| 134 | idx.colID[c.Pos] = c.ID |
| 135 | } |
| 136 | return idx |
| 137 | } |
| 138 | |
| 139 | // extent is how far a tab's content actually reaches: one past the |
| 140 | // furthest row and column that hold a cell. |
| 141 | // |
| 142 | // It exists because the grid page opened a CONSTANT window. 100 rows is |
| 143 | // the right default for a new sheet and the wrong answer for one whose |
| 144 | // content runs to row 151 — the cell route accepts that write, the table |
| 145 | // view lists it and the export carries it, and the grid simply had no |
| 146 | // row 151 to scroll to. Nothing was lost and nothing said anything. |
| 147 | // |
| 148 | // Positions, not ids: a cell records the row and column it belongs to by |
| 149 | // id, and only the index knows where those sit. A row record with no |
| 150 | // cells in it does not count, which is deliberate — EnsureRow is called |
| 151 | // on the way to a write, so counting rows rather than cells would widen |
| 152 | // the window by whatever a failed write left behind. |
| 153 | func extent(h *store.Handle, idx posIndex) (rows, cols int) { |
| 154 | coords, err := h.Cells() |
| 155 | if err != nil { |
| 156 | return 0, 0 |
| 157 | } |
| 158 | for _, c := range coords { |
| 159 | if r, ok := idx.rowPos[c.RowID]; ok && r+1 > rows { |
| 160 | rows = r + 1 |
| 161 | } |
| 162 | if col, ok := idx.colPos[c.ColID]; ok && col+1 > cols { |
| 163 | cols = col + 1 |
| 164 | } |
| 165 | } |
| 166 | return rows, cols |
| 167 | } |
| 168 | |
| 169 | // snapshot reads every cell inside a window. |
| 170 | // |
| 171 | // Sparse: only cells that exist are sent. A blank cell is absent from |
| 172 | // the response rather than present and empty, which is the difference |
| 173 | // between a 100×26 window costing two kilobytes and costing sixty. |
| 174 | func snapshot(h *store.Handle, idx posIndex, rows, cols int) ([]gridCell, styleTable, error) { |
| 175 | return snapshotWindow(h, idx, 0, 0, rows, cols) |
| 176 | } |
| 177 | |
| 178 | func snapshotWindow(h *store.Handle, idx posIndex, firstRow, firstCol, rows, cols int) ([]gridCell, styleTable, error) { |
| 179 | coords, err := h.Cells() |
| 180 | if err != nil { |
| 181 | return nil, nil, err |
| 182 | } |
| 183 | // One batched read for the whole window, not one query per cell: |
| 184 | // see store.Handle.Formats for why Handle.Format is the wrong shape |
| 185 | // here now that every cell's style is painted. |
| 186 | formats, err := h.Formats() |
| 187 | if err != nil { |
| 188 | return nil, nil, err |
| 189 | } |
| 190 | col := newCollector(formats) |
| 191 | out := make([]gridCell, 0, len(coords)) |
| 192 | for _, c := range coords { |
| 193 | r, ok := idx.rowPos[c.RowID] |
| 194 | if !ok || r < firstRow || r >= firstRow+rows { |
| 195 | continue |
| 196 | } |
| 197 | colPos, ok := idx.colPos[c.ColID] |
| 198 | if !ok || colPos < firstCol || colPos >= firstCol+cols { |
| 199 | continue |
| 200 | } |
| 201 | raw, _ := h.RawInput(c.RowID, c.ColID) |
| 202 | out = append(out, gridCell{ |
| 203 | R: r, C: colPos, |
| 204 | Raw: raw, |
| 205 | Display: display(h.Value(c.RowID, c.ColID), col.numFmtFor(c.RowID, c.ColID)), |
| 206 | Num: isNumber(h.Value(c.RowID, c.ColID)), |
| 207 | F: col.keyFor(c.RowID, c.ColID), |
| 208 | }) |
| 209 | } |
| 210 | return out, col.done(), nil |
| 211 | } |
| 212 | |
| 213 | // fromReport turns a write's report into wire cells. |
| 214 | // |
| 215 | // A reported cell whose row or column is somehow not in the index is |
| 216 | // dropped rather than sent with a wrong coordinate: a cell painted in |
| 217 | // the wrong place is worse than a cell not painted, because nothing |
| 218 | // later corrects it. |
| 219 | func fromReport(rep store.Report, idx posIndex, h *store.Handle) ([]gridCell, styleTable) { |
| 220 | formats, err := h.Formats() |
| 221 | if err != nil { |
| 222 | // A format read that fails costs styling on this batch, not the |
| 223 | // edit itself. The values are what the person typed; painting |
| 224 | // them unstyled and letting the next snapshot correct it beats |
| 225 | // failing a write that already landed in storage. |
| 226 | formats = nil |
| 227 | } |
| 228 | col := newCollector(formats) |
| 229 | out := make([]gridCell, 0, len(rep.Cells)) |
| 230 | for _, c := range rep.Cells { |
| 231 | r, ok := idx.rowPos[c.RowID] |
| 232 | if !ok { |
| 233 | continue |
| 234 | } |
| 235 | colPos, ok := idx.colPos[c.ColID] |
| 236 | if !ok { |
| 237 | continue |
| 238 | } |
| 239 | out = append(out, gridCell{ |
| 240 | R: r, C: colPos, |
| 241 | Raw: c.Raw, |
| 242 | Display: display(c.Value, col.numFmtFor(c.RowID, c.ColID)), |
| 243 | Derived: c.Derived, |
| 244 | Num: isNumber(c.Value), |
| 245 | F: col.keyFor(c.RowID, c.ColID), |
| 246 | }) |
| 247 | } |
| 248 | return out, col.done() |
| 249 | } |
| 250 | |
| 251 | // clamp bounds a requested window. |
| 252 | // |
| 253 | // Clamped, not refused: a request for a million rows is a client bug or |
| 254 | // a curious operator, and answering 400 turns either into a blank page |
| 255 | // where answering with 500 rows turns it into a working one. |
| 256 | func clamp(want, def, max int) int { |
| 257 | if want <= 0 { |
| 258 | return def |
| 259 | } |
| 260 | if want > max { |
| 261 | return max |
| 262 | } |
| 263 | return want |
| 264 | } |
| 265 | |
| 266 | // marshalGrid encodes a response for the wire. |
| 267 | // |
| 268 | // Errors are impossible for this shape — every field is a string, an |
| 269 | // int or a bool — but the signature keeps the caller honest rather than |
| 270 | // hiding a discarded error behind an underscore at the point where cell |
| 271 | // content would be the thing lost. |
| 272 | func marshalGrid(resp gridResponse) ([]byte, error) { return json.Marshal(resp) } |
| 273 | |