vastgrid 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +175 -13
  2. package/dist/canvas/geometry.d.ts +92 -0
  3. package/dist/canvas/geometry.d.ts.map +1 -0
  4. package/dist/canvas/geometry.js +197 -0
  5. package/dist/canvas/geometry.js.map +1 -0
  6. package/dist/canvas/grid-canvas.d.ts +161 -0
  7. package/dist/canvas/grid-canvas.d.ts.map +1 -0
  8. package/dist/canvas/grid-canvas.js +1146 -0
  9. package/dist/canvas/grid-canvas.js.map +1 -0
  10. package/dist/canvas/hooks.d.ts +61 -0
  11. package/dist/canvas/hooks.d.ts.map +1 -0
  12. package/dist/canvas/hooks.js +2 -0
  13. package/dist/canvas/hooks.js.map +1 -0
  14. package/dist/canvas/keys.d.ts +26 -0
  15. package/dist/canvas/keys.d.ts.map +1 -0
  16. package/dist/canvas/keys.js +133 -0
  17. package/dist/canvas/keys.js.map +1 -0
  18. package/dist/canvas/layout.d.ts +26 -0
  19. package/dist/canvas/layout.d.ts.map +1 -0
  20. package/dist/canvas/layout.js +73 -0
  21. package/dist/canvas/layout.js.map +1 -0
  22. package/dist/canvas/scroll.d.ts +31 -0
  23. package/dist/canvas/scroll.d.ts.map +1 -0
  24. package/dist/canvas/scroll.js +111 -0
  25. package/dist/canvas/scroll.js.map +1 -0
  26. package/dist/canvas/text.d.ts +23 -0
  27. package/dist/canvas/text.d.ts.map +1 -0
  28. package/dist/canvas/text.js +142 -0
  29. package/dist/canvas/text.js.map +1 -0
  30. package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
  31. package/dist/canvas/theme.d.ts.map +1 -0
  32. package/dist/canvas/theme.js +62 -0
  33. package/dist/canvas/theme.js.map +1 -0
  34. package/dist/core/cache.d.ts +48 -0
  35. package/dist/core/cache.d.ts.map +1 -0
  36. package/dist/core/cache.js +222 -0
  37. package/dist/core/cache.js.map +1 -0
  38. package/dist/core/checked.d.ts +20 -0
  39. package/dist/core/checked.d.ts.map +1 -0
  40. package/dist/core/checked.js +61 -0
  41. package/dist/core/checked.js.map +1 -0
  42. package/dist/core/client.d.ts +33 -0
  43. package/dist/core/client.d.ts.map +1 -0
  44. package/dist/core/client.js +103 -0
  45. package/dist/core/client.js.map +1 -0
  46. package/dist/core/conditions.d.ts +48 -0
  47. package/dist/core/conditions.d.ts.map +1 -0
  48. package/dist/core/conditions.js +105 -0
  49. package/dist/core/conditions.js.map +1 -0
  50. package/dist/core/evaluate.d.ts +20 -0
  51. package/dist/core/evaluate.d.ts.map +1 -0
  52. package/dist/core/evaluate.js +210 -0
  53. package/dist/core/evaluate.js.map +1 -0
  54. package/dist/core/filters.d.ts +51 -0
  55. package/dist/core/filters.d.ts.map +1 -0
  56. package/dist/core/filters.js +303 -0
  57. package/dist/core/filters.js.map +1 -0
  58. package/dist/core/format.d.ts +14 -0
  59. package/dist/core/format.d.ts.map +1 -0
  60. package/dist/core/format.js +303 -0
  61. package/dist/core/format.js.map +1 -0
  62. package/dist/core/measures.d.ts +26 -0
  63. package/dist/core/measures.d.ts.map +1 -0
  64. package/dist/core/measures.js +105 -0
  65. package/dist/core/measures.js.map +1 -0
  66. package/dist/core/request-ids.d.ts +20 -0
  67. package/dist/core/request-ids.d.ts.map +1 -0
  68. package/dist/core/request-ids.js +55 -0
  69. package/dist/core/request-ids.js.map +1 -0
  70. package/dist/core/rows.d.ts +79 -0
  71. package/dist/core/rows.d.ts.map +1 -0
  72. package/dist/core/rows.js +173 -0
  73. package/dist/core/rows.js.map +1 -0
  74. package/dist/core/store.d.ts +233 -0
  75. package/dist/core/store.d.ts.map +1 -0
  76. package/dist/core/store.js +1162 -0
  77. package/dist/core/store.js.map +1 -0
  78. package/dist/core/totals.d.ts +24 -0
  79. package/dist/core/totals.d.ts.map +1 -0
  80. package/dist/core/totals.js +49 -0
  81. package/dist/core/totals.js.map +1 -0
  82. package/dist/core/trace.d.ts +7 -0
  83. package/dist/core/trace.d.ts.map +1 -0
  84. package/dist/core/trace.js +26 -0
  85. package/dist/core/trace.js.map +1 -0
  86. package/dist/core/transports.d.ts +50 -0
  87. package/dist/core/transports.d.ts.map +1 -0
  88. package/dist/core/transports.js +160 -0
  89. package/dist/core/transports.js.map +1 -0
  90. package/dist/core/types.d.ts +150 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +45 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/core.d.ts +12 -0
  95. package/dist/core.d.ts.map +1 -0
  96. package/dist/core.js +11 -0
  97. package/dist/core.js.map +1 -0
  98. package/dist/index.d.ts +15 -6
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +13 -7
  101. package/dist/index.js.map +1 -1
  102. package/dist/react/accessible-rows.d.ts +13 -0
  103. package/dist/react/accessible-rows.d.ts.map +1 -0
  104. package/dist/react/accessible-rows.js +320 -0
  105. package/dist/react/accessible-rows.js.map +1 -0
  106. package/dist/react/cell-editor.d.ts +22 -0
  107. package/dist/react/cell-editor.d.ts.map +1 -0
  108. package/dist/react/cell-editor.js +431 -0
  109. package/dist/react/cell-editor.js.map +1 -0
  110. package/dist/react/cell-menu.d.ts +18 -0
  111. package/dist/react/cell-menu.d.ts.map +1 -0
  112. package/dist/react/cell-menu.js +299 -0
  113. package/dist/react/cell-menu.js.map +1 -0
  114. package/dist/react/column-menu.d.ts +16 -0
  115. package/dist/react/column-menu.d.ts.map +1 -0
  116. package/dist/react/column-menu.js +1311 -0
  117. package/dist/react/column-menu.js.map +1 -0
  118. package/dist/react/columns-menu.d.ts +7 -0
  119. package/dist/react/columns-menu.d.ts.map +1 -0
  120. package/dist/react/columns-menu.js +249 -0
  121. package/dist/react/columns-menu.js.map +1 -0
  122. package/dist/react/conditions-menu.d.ts +10 -0
  123. package/dist/react/conditions-menu.d.ts.map +1 -0
  124. package/dist/react/conditions-menu.js +795 -0
  125. package/dist/react/conditions-menu.js.map +1 -0
  126. package/dist/react/context.d.ts +29 -0
  127. package/dist/react/context.d.ts.map +1 -0
  128. package/dist/react/context.js +18 -0
  129. package/dist/react/context.js.map +1 -0
  130. package/dist/react/data-grid.d.ts +79 -0
  131. package/dist/react/data-grid.d.ts.map +1 -0
  132. package/dist/react/data-grid.js +653 -0
  133. package/dist/react/data-grid.js.map +1 -0
  134. package/dist/react/detail-panel.d.ts +22 -0
  135. package/dist/react/detail-panel.d.ts.map +1 -0
  136. package/dist/react/detail-panel.js +242 -0
  137. package/dist/react/detail-panel.js.map +1 -0
  138. package/dist/react/export-menu.d.ts +11 -0
  139. package/dist/react/export-menu.d.ts.map +1 -0
  140. package/dist/react/export-menu.js +304 -0
  141. package/dist/react/export-menu.js.map +1 -0
  142. package/dist/react/find-bar.d.ts +11 -0
  143. package/dist/react/find-bar.d.ts.map +1 -0
  144. package/dist/react/find-bar.js +303 -0
  145. package/dist/react/find-bar.js.map +1 -0
  146. package/dist/react/format-form.d.ts +12 -0
  147. package/dist/react/format-form.d.ts.map +1 -0
  148. package/dist/react/format-form.js +863 -0
  149. package/dist/react/format-form.js.map +1 -0
  150. package/dist/react/formula-menu.d.ts +6 -0
  151. package/dist/react/formula-menu.d.ts.map +1 -0
  152. package/dist/react/formula-menu.js +343 -0
  153. package/dist/react/formula-menu.js.map +1 -0
  154. package/dist/react/grid-body.d.ts +33 -0
  155. package/dist/react/grid-body.d.ts.map +1 -0
  156. package/dist/react/grid-body.js +1389 -0
  157. package/dist/react/grid-body.js.map +1 -0
  158. package/dist/react/group-menu.d.ts +7 -0
  159. package/dist/react/group-menu.d.ts.map +1 -0
  160. package/dist/react/group-menu.js +508 -0
  161. package/dist/react/group-menu.js.map +1 -0
  162. package/dist/react/hooks.d.ts +33 -0
  163. package/dist/react/hooks.d.ts.map +1 -0
  164. package/dist/react/hooks.js +161 -0
  165. package/dist/react/hooks.js.map +1 -0
  166. package/dist/react/messages.d.ts +180 -0
  167. package/dist/react/messages.d.ts.map +1 -0
  168. package/dist/react/messages.js +414 -0
  169. package/dist/react/messages.js.map +1 -0
  170. package/dist/react/save.d.ts +9 -0
  171. package/dist/react/save.d.ts.map +1 -0
  172. package/dist/react/save.js +102 -0
  173. package/dist/react/save.js.map +1 -0
  174. package/dist/react/static-rows.d.ts +9 -0
  175. package/dist/react/static-rows.d.ts.map +1 -0
  176. package/dist/react/static-rows.js +176 -0
  177. package/dist/react/static-rows.js.map +1 -0
  178. package/dist/react/status-bar.d.ts +12 -0
  179. package/dist/react/status-bar.d.ts.map +1 -0
  180. package/dist/react/status-bar.js +413 -0
  181. package/dist/react/status-bar.js.map +1 -0
  182. package/dist/react/toolbar-popover.d.ts +14 -0
  183. package/dist/react/toolbar-popover.d.ts.map +1 -0
  184. package/dist/react/toolbar-popover.js +143 -0
  185. package/dist/react/toolbar-popover.js.map +1 -0
  186. package/dist/react/toolbar.d.ts +13 -0
  187. package/dist/react/toolbar.d.ts.map +1 -0
  188. package/dist/react/toolbar.js +344 -0
  189. package/dist/react/toolbar.js.map +1 -0
  190. package/dist/server.d.ts +22 -0
  191. package/dist/server.d.ts.map +1 -0
  192. package/dist/server.js +45 -0
  193. package/dist/server.js.map +1 -0
  194. package/dist/styles.css +496 -316
  195. package/package.json +43 -18
  196. package/src/canvas/geometry.ts +197 -0
  197. package/src/canvas/grid-canvas.ts +1222 -0
  198. package/src/canvas/hooks.ts +61 -0
  199. package/src/canvas/keys.ts +96 -0
  200. package/src/canvas/layout.ts +77 -0
  201. package/src/canvas/scroll.ts +120 -0
  202. package/src/canvas/text.ts +155 -0
  203. package/src/{theme.ts → canvas/theme.ts} +14 -1
  204. package/src/core/cache.ts +247 -0
  205. package/src/core/checked.ts +64 -0
  206. package/src/core/client.ts +103 -0
  207. package/src/core/conditions.ts +89 -0
  208. package/src/core/evaluate.ts +221 -0
  209. package/src/core/filters.ts +268 -0
  210. package/src/core/format.ts +309 -0
  211. package/src/core/measures.ts +99 -0
  212. package/src/core/request-ids.ts +54 -0
  213. package/src/core/rows.ts +213 -0
  214. package/src/core/store.ts +1122 -0
  215. package/src/core/totals.ts +59 -0
  216. package/src/core/trace.ts +44 -0
  217. package/src/core/transports.ts +177 -0
  218. package/src/core/types.ts +168 -0
  219. package/src/core.ts +39 -0
  220. package/src/index.ts +24 -12
  221. package/src/react/accessible-rows.tsx +242 -0
  222. package/src/react/cell-editor.tsx +216 -0
  223. package/src/react/cell-menu.tsx +166 -0
  224. package/src/react/column-menu.tsx +619 -0
  225. package/src/react/columns-menu.tsx +116 -0
  226. package/src/react/conditions-menu.tsx +289 -0
  227. package/src/react/context.ts +33 -0
  228. package/src/react/data-grid.tsx +286 -0
  229. package/src/react/detail-panel.tsx +92 -0
  230. package/src/react/export-menu.tsx +112 -0
  231. package/src/react/find-bar.tsx +112 -0
  232. package/src/react/format-form.tsx +421 -0
  233. package/src/react/formula-menu.tsx +163 -0
  234. package/src/react/grid-body.tsx +744 -0
  235. package/src/react/group-menu.tsx +214 -0
  236. package/src/react/hooks.ts +104 -0
  237. package/src/react/messages.ts +556 -0
  238. package/src/react/save.ts +42 -0
  239. package/src/react/static-rows.tsx +59 -0
  240. package/src/react/status-bar.tsx +175 -0
  241. package/src/react/toolbar-popover.tsx +69 -0
  242. package/src/react/toolbar.tsx +152 -0
  243. package/src/server.ts +58 -0
  244. package/src/styles.css +496 -316
  245. package/dist/components/bars.d.ts +0 -14
  246. package/dist/components/bars.d.ts.map +0 -1
  247. package/dist/components/bars.js +0 -67
  248. package/dist/components/bars.js.map +0 -1
  249. package/dist/components/cell-inspector.d.ts +0 -11
  250. package/dist/components/cell-inspector.d.ts.map +0 -1
  251. package/dist/components/cell-inspector.js +0 -37
  252. package/dist/components/cell-inspector.js.map +0 -1
  253. package/dist/components/columns-panel.d.ts +0 -12
  254. package/dist/components/columns-panel.d.ts.map +0 -1
  255. package/dist/components/columns-panel.js +0 -11
  256. package/dist/components/columns-panel.js.map +0 -1
  257. package/dist/components/filter-panel.d.ts +0 -17
  258. package/dist/components/filter-panel.d.ts.map +0 -1
  259. package/dist/components/filter-panel.js +0 -165
  260. package/dist/components/filter-panel.js.map +0 -1
  261. package/dist/components/find-bar.d.ts +0 -9
  262. package/dist/components/find-bar.d.ts.map +0 -1
  263. package/dist/components/find-bar.js +0 -46
  264. package/dist/components/find-bar.js.map +0 -1
  265. package/dist/components/popover.d.ts +0 -38
  266. package/dist/components/popover.d.ts.map +0 -1
  267. package/dist/components/popover.js +0 -104
  268. package/dist/components/popover.js.map +0 -1
  269. package/dist/components/stats-panel.d.ts +0 -12
  270. package/dist/components/stats-panel.d.ts.map +0 -1
  271. package/dist/components/stats-panel.js +0 -40
  272. package/dist/components/stats-panel.js.map +0 -1
  273. package/dist/controller.d.ts +0 -218
  274. package/dist/controller.d.ts.map +0 -1
  275. package/dist/controller.js +0 -788
  276. package/dist/controller.js.map +0 -1
  277. package/dist/data-grid.d.ts +0 -51
  278. package/dist/data-grid.d.ts.map +0 -1
  279. package/dist/data-grid.js +0 -698
  280. package/dist/data-grid.js.map +0 -1
  281. package/dist/labels.d.ts +0 -9
  282. package/dist/labels.d.ts.map +0 -1
  283. package/dist/labels.js +0 -38
  284. package/dist/labels.js.map +0 -1
  285. package/dist/messages.d.ts +0 -99
  286. package/dist/messages.d.ts.map +0 -1
  287. package/dist/messages.js +0 -276
  288. package/dist/messages.js.map +0 -1
  289. package/dist/renderer.d.ts +0 -52
  290. package/dist/renderer.d.ts.map +0 -1
  291. package/dist/renderer.js +0 -522
  292. package/dist/renderer.js.map +0 -1
  293. package/dist/text.d.ts +0 -14
  294. package/dist/text.d.ts.map +0 -1
  295. package/dist/text.js +0 -63
  296. package/dist/text.js.map +0 -1
  297. package/dist/theme.d.ts.map +0 -1
  298. package/dist/theme.js +0 -54
  299. package/dist/theme.js.map +0 -1
  300. package/src/components/bars.tsx +0 -179
  301. package/src/components/cell-inspector.tsx +0 -70
  302. package/src/components/columns-panel.tsx +0 -59
  303. package/src/components/filter-panel.tsx +0 -290
  304. package/src/components/find-bar.tsx +0 -104
  305. package/src/components/popover.tsx +0 -194
  306. package/src/components/stats-panel.tsx +0 -153
  307. package/src/controller.ts +0 -887
  308. package/src/data-grid.tsx +0 -1029
  309. package/src/labels.ts +0 -46
  310. package/src/messages.ts +0 -390
  311. package/src/renderer.ts +0 -535
  312. package/src/text.ts +0 -55
@@ -0,0 +1,1162 @@
1
+ import { applyCommand, CommandError, canonicalJson, checkPlan, decodeBlock, displayedColumns, emptyDocument, emptySession, keyText, normalizePlan, PlanError, planOptions, repairDocument } from "vastgrid-spec";
2
+ import { ViewCache } from "./cache.js";
3
+ import { GridClient } from "./client.js";
4
+ import { valuesPlan } from "./filters.js";
5
+ import { measuresPlan } from "./measures.js";
6
+ import { RowReader, sameColumns } from "./rows.js";
7
+ import { TotalsReader } from "./totals.js";
8
+ import { now, track } from "./trace.js";
9
+ import { GridError } from "./types.js";
10
+
11
+ /** Bytes of rows that a store keeps for the view on screen, by default. */
12
+ export const CACHE_BYTES = 64 * 1024 * 1024;
13
+
14
+ /** Failures of a write that the same write may not meet when it is sent again. */
15
+ const RETRIED = new Set(["internal", "cancelled", "not_ready", "table_not_found"]);
16
+
17
+ /** A command applied, or why it was not. */
18
+
19
+ function isCancelled(error) {
20
+ return error instanceof GridError && error.code === "cancelled";
21
+ }
22
+ function asGridError(error) {
23
+ if (error instanceof GridError) return error;
24
+ if (error instanceof PlanError) {
25
+ return new GridError("invalid_plan", error.message, {
26
+ reason: error.reason,
27
+ path: error.path
28
+ });
29
+ }
30
+ return new GridError("internal", error instanceof Error ? error.message : String(error));
31
+ }
32
+ function planKey(plan) {
33
+ return canonicalJson(normalizePlan(plan));
34
+ }
35
+
36
+ /**
37
+ * The grid without a screen (§4 of react.md): the state of the specification as immutable snapshots, the
38
+ * commands that change it, and the rows of the view on screen, read through a transport. Making a store has
39
+ * no side effects; `connect` opens the table and starts reading.
40
+ */
41
+ export class GridStore {
42
+ listeners = new Set();
43
+ rowListeners = new Set();
44
+ events = {
45
+ document: new Set(),
46
+ session: new Set(),
47
+ error: new Set()
48
+ };
49
+ controlled = {
50
+ document: false,
51
+ session: false
52
+ };
53
+ /** What a command made of a controlled part, until the host passes it back (§5.2 of react.md). */
54
+ proposed = {};
55
+ client = null;
56
+ /** Counts connections, so that answers to an earlier one are dropped. */
57
+ connection = 0;
58
+ /** Whether the table is open for the current connection. */
59
+ opened = false;
60
+ current = null;
61
+ next = null;
62
+ /**
63
+ * Values of writes (§7.2) that the rows read do not hold yet, by the key text of their row and by column, with
64
+ * the write that gave them: the grid shows them at once, and drops them when the write fails or once the view
65
+ * of its edits version is on screen.
66
+ */
67
+ written = new Map();
68
+ writes = 0;
69
+ /** The edits version that each write that succeeded made. */
70
+ writeVersions = new Map();
71
+ /** Settles once the writes sent so far are answered and their steps are in the history. */
72
+ writing = Promise.resolve();
73
+ totals = new TotalsReader((plan, revision, signal) => this.measure(plan, revision, signal), totals => this.publish({
74
+ totals
75
+ }), error => this.readFailed(error));
76
+ rowsVersion = 0;
77
+ constructor(options) {
78
+ this.options = options;
79
+ this.reader = new RowReader({
80
+ current: () => this.current,
81
+ next: () => this.next,
82
+ client: () => this.client,
83
+ table: () => this.snapshot.table,
84
+ opened: () => this.opened,
85
+ rowKey: () => this.base("document").rowKey,
86
+ keyed: () => Boolean(this.base("session").active?.key),
87
+ showView: info => this.publish({
88
+ view: info
89
+ }),
90
+ showRows: () => this.emitRows(),
91
+ fail: error => this.readFailed(error)
92
+ }, options.cacheBytes ?? CACHE_BYTES);
93
+ this.snapshot = {
94
+ document: options.document ?? emptyDocument(),
95
+ session: options.session ?? emptySession(),
96
+ table: null,
97
+ view: null,
98
+ displayed: [],
99
+ pending: null,
100
+ status: "idle",
101
+ error: null,
102
+ failedWrite: null,
103
+ totals: null
104
+ };
105
+ if (options.initial) this.seed(options.initial);
106
+ }
107
+
108
+ // ----- reading the state -----
109
+
110
+ getSnapshot = () => this.snapshot;
111
+ subscribe = listener => {
112
+ this.listeners.add(listener);
113
+ return () => {
114
+ this.listeners.delete(listener);
115
+ };
116
+ };
117
+
118
+ /** Follows changes of the document or the session (reported also when they are controlled), and errors. */
119
+ on(event, listener) {
120
+ const set = this.events[event];
121
+ set.add(listener);
122
+ return () => {
123
+ set.delete(listener);
124
+ };
125
+ }
126
+ emit(event, value) {
127
+ for (const listener of this.events[event]) listener(value);
128
+ }
129
+ publish(changes) {
130
+ const previous = this.snapshot;
131
+ const next = {
132
+ ...previous,
133
+ ...changes
134
+ };
135
+ if (next.view && previous.view && sameColumns(next.view.columns, previous.view.columns)) {
136
+ // Views read again keep their columns' identity, which displayed columns and commands key on.
137
+ if (next.view.columns !== previous.view.columns) next.view = {
138
+ ...next.view,
139
+ columns: previous.view.columns
140
+ };
141
+ }
142
+ const keep = next.document.columns === previous.document.columns && next.view?.columns === previous.view?.columns;
143
+ next.displayed = keep ? previous.displayed : this.displayedOf(next);
144
+ this.snapshot = next;
145
+ for (const listener of this.listeners) listener();
146
+ // The totals are read once the table is open, which rows read on a server come before.
147
+ if (this.opened) this.totals.follow(this.current, this.snapshot);
148
+ }
149
+
150
+ /** Reads the one row of a plan that aggregates without keys: its values and their types, by column name. */
151
+ async measure(plan, revision, signal) {
152
+ const table = this.snapshot.table;
153
+ const client = this.client;
154
+ if (!table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
155
+ const names = plan.aggregate?.measures.map(measure => measure.name) ?? [];
156
+ const result = await client.request("rows", {
157
+ table: table.table,
158
+ revision,
159
+ plan,
160
+ start: 0,
161
+ end: 1,
162
+ columns: names
163
+ }, signal);
164
+ const block = decodeBlock(result.block.bytes);
165
+ const values = {};
166
+ const columns = {};
167
+ block.columns.forEach((column, index) => {
168
+ values[column.name] = block.rows[0]?.[index] ?? null;
169
+ columns[column.name] = column;
170
+ });
171
+ return {
172
+ values,
173
+ columns
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Measures (§4.5) over the rows of the document's plan, for the column statistics: an `aggregate` without keys
179
+ * (`measuresPlan`). Fails for a plan that groups rows.
180
+ */
181
+ async aggregate(measures, signal) {
182
+ const plan = measuresPlan(this.base("document").plan ?? {}, measures);
183
+ if (!plan) throw new GridError("invalid_plan", "The measures of a grouped view are not read.");
184
+ return this.measure(plan, this.current?.revision ?? this.snapshot.table?.revision ?? null, signal);
185
+ }
186
+ displayedOf(snapshot) {
187
+ return snapshot.view ? displayedColumns(snapshot.document, snapshot.view.columns) : [];
188
+ }
189
+
190
+ /** The state that commands apply to: what was proposed for controlled parts, or what is shown. */
191
+ base(part) {
192
+ return this.proposed[part] ?? this.snapshot[part];
193
+ }
194
+
195
+ // ----- rows -----
196
+
197
+ /** The value of a cell of the view on screen; undefined while it is not read. */
198
+ cell(row, column) {
199
+ const view = this.current;
200
+ const value = view?.cache.value(row, column);
201
+ if (!view || this.written.size === 0) return value;
202
+ const key = view.cache.loaded(row, this.base("document").rowKey)?.key;
203
+ const entry = key === undefined ? undefined : this.written.get(keyText(key))?.get(column);
204
+ return entry === undefined ? value : entry.value;
205
+ }
206
+
207
+ /** Whether the engine cut a cell's value to the text limit (§8.4). */
208
+ isCut(row, column) {
209
+ return this.current?.cache.isCut(row, column) ?? false;
210
+ }
211
+
212
+ /** The key of a row of the view on screen (§5.3); undefined while it is not read. */
213
+ rowKey(row) {
214
+ return this.current?.cache.loaded(row, this.base("document").rowKey)?.key;
215
+ }
216
+
217
+ /** The row ID of a row of the view on screen. */
218
+ rowId(row) {
219
+ return this.current?.cache.rowId(row);
220
+ }
221
+
222
+ /** Changes whenever rows of the view on screen arrive or leave. */
223
+ getRowsVersion = () => this.rowsVersion;
224
+
225
+ /** Follows the arrival of rows (the snapshot does not change when rows arrive). */
226
+ subscribeRows = listener => {
227
+ this.rowListeners.add(listener);
228
+ return () => {
229
+ this.rowListeners.delete(listener);
230
+ };
231
+ };
232
+ emitRows() {
233
+ this.rowsVersion++;
234
+ for (const listener of this.rowListeners) listener();
235
+ }
236
+
237
+ /** Tells the store what the canvas shows, so that it reads those rows and a page around them. */
238
+ setViewport(viewport) {
239
+ this.reader.setViewport(viewport);
240
+ }
241
+
242
+ /** Reads rows `[start, end)` of `columns` of the view on screen if they are not read, for cells React renders. */
243
+ ensure(start, end, columns) {
244
+ this.reader.ensure(start, end, columns);
245
+ }
246
+
247
+ // ----- connecting -----
248
+
249
+ /**
250
+ * Opens the table, follows its events and reads the view of the document's plan. Gives the function that
251
+ * undoes all of it; connecting again afterwards works (Strict Mode does it, §4.3 of react.md).
252
+ */
253
+ connect() {
254
+ const connection = ++this.connection;
255
+ const client = new GridClient(this.options.transport);
256
+ this.client = client;
257
+ this.opened = false;
258
+ let connected = true;
259
+ // Events can arrive before the open response names the table (HTTP's streams are independent). Replay
260
+ // them against that response, so an early ready event cannot leave plans waiting for loading forever.
261
+ let openingEvents = [];
262
+ const stopEvents = client.subscribe(event => {
263
+ if (!connected || connection !== this.connection) return;
264
+ if (openingEvents) openingEvents.push(event);else this.onEvent(event, connection);
265
+ });
266
+ if (this.snapshot.status === "idle" || this.snapshot.status === "error") this.publish({
267
+ status: "opening"
268
+ });
269
+ let tableId = null;
270
+ const close = id => void client.request("close", {
271
+ table: id
272
+ }).catch(() => {});
273
+ void (async () => {
274
+ try {
275
+ // Not cancelled: an engine may open the table all the same, which is then closed.
276
+ const info = await client.request("open", {
277
+ ref: this.options.table,
278
+ ...(this.options.open ? {
279
+ read: this.options.open
280
+ } : {})
281
+ });
282
+ if (!connected) {
283
+ close(info.table);
284
+ return;
285
+ }
286
+ tableId = info.table;
287
+ this.opened = true;
288
+ this.opening(info);
289
+ const events = openingEvents;
290
+ openingEvents = null;
291
+ for (const event of events ?? []) this.onEvent(event, connection);
292
+ } catch (error) {
293
+ openingEvents = null;
294
+ if (connected && !isCancelled(error)) {
295
+ this.publish({
296
+ status: "error"
297
+ });
298
+ this.fail(asGridError(error));
299
+ }
300
+ }
301
+ })();
302
+ return () => {
303
+ if (!connected) return;
304
+ connected = false;
305
+ openingEvents = null;
306
+ stopEvents();
307
+ if (tableId !== null) close(tableId);
308
+ if (connection !== this.connection) return;
309
+ this.connection++;
310
+ this.opened = false;
311
+ this.current?.abort.abort();
312
+ this.next?.abort.abort();
313
+ this.next = null;
314
+ this.totals.reset();
315
+ if (this.current) this.current = {
316
+ ...this.current,
317
+ abort: new AbortController()
318
+ };
319
+ this.client = null;
320
+ };
321
+ }
322
+
323
+ /** The table is open: repairs the document for it and reads the view (§5.5). */
324
+ opening(info) {
325
+ const document = this.base("document");
326
+ const repaired = repairDocument(document, info.columns, planOptions(info.capabilities));
327
+ const seeded = this.current;
328
+ // Rows read on a server stay when they belong to the same revision and plan.
329
+ if (seeded && (seeded.revision !== info.revision || seeded.edits !== info.edits.version)) this.current = null;else if (seeded) this.current = {
330
+ ...seeded,
331
+ abort: new AbortController()
332
+ };
333
+ this.publish({
334
+ table: info,
335
+ status: info.status === "error" ? "error" : "ready"
336
+ });
337
+ if (repaired.removed.length > 0) this.change(repaired.document, this.base("session"));
338
+ this.showPlan(this.base("document").plan ?? {});
339
+ this.reader.schedule();
340
+ }
341
+ onEvent(event, connection) {
342
+ if (connection !== this.connection) return;
343
+ const table = this.snapshot.table;
344
+ if (!table || event.table !== table.table) return;
345
+ if (event.event === "changed") {
346
+ const revision = event.revision;
347
+ const edits = event.edits;
348
+ const fileChanged = event.fileChanged === true;
349
+ if (revision === table.revision) {
350
+ if (edits.version === table.edits.version && fileChanged === (table.fileChanged === true)) return;
351
+ const {
352
+ fileChanged: _,
353
+ ...rest
354
+ } = table;
355
+ this.publish({
356
+ table: fileChanged ? {
357
+ ...rest,
358
+ edits,
359
+ fileChanged
360
+ } : {
361
+ ...rest,
362
+ edits
363
+ }
364
+ });
365
+ this.showPlan(this.base("document").plan ?? {});
366
+ return;
367
+ }
368
+ // Another revision may have other columns: the table is read again, then the view (§5.4).
369
+ const client = this.client;
370
+ void client?.request("info", {
371
+ table: table.table
372
+ }).then(info => {
373
+ if (connection === this.connection) this.adopt(info);
374
+ }).catch(error => {
375
+ if (connection === this.connection && !isCancelled(error)) this.fail(asGridError(error));
376
+ });
377
+ } else if (event.event === "progress") {
378
+ this.publish({
379
+ table: {
380
+ ...table,
381
+ status: event.status,
382
+ progress: event.progress,
383
+ readableRows: event.readableRows,
384
+ rowCount: event.rowCount
385
+ }
386
+ });
387
+ if (event.status === "ready") this.showPlan(this.base("document").plan ?? {});
388
+ // A view still being made reads its rows again once the table is read, also without `view` events (§9).
389
+ const view = this.current;
390
+ if (event.status === "ready" && view?.info && view.info.status !== "ready") {
391
+ const columns = this.reader.withKeyFor(view, this.reader.viewport.columns);
392
+ void this.reader.read(view, this.reader.around(this.reader.viewport.top, null, columns)).then(() => this.reader.schedule(), () => {});
393
+ }
394
+ } else if (event.event === "view") {
395
+ const view = [this.next, this.current].find(item => item?.info?.hash === event.hash && item?.revision === table.revision);
396
+ if (view?.info) {
397
+ view.info = {
398
+ ...view.info,
399
+ status: event.status,
400
+ readableRows: event.readableRows,
401
+ rowCount: event.rowCount,
402
+ rowCountExact: event.rowCountExact
403
+ };
404
+ if (view === this.current) this.publish({
405
+ view: view.info
406
+ });
407
+ this.reader.schedule();
408
+ }
409
+ }
410
+ }
411
+ fail(error) {
412
+ this.publish({
413
+ error
414
+ });
415
+ this.emit("error", error);
416
+ }
417
+
418
+ /** A read failed: unless it was cancelled, the failure belongs to no command (§4.2 of react.md). */
419
+ readFailed(error) {
420
+ if (!isCancelled(error)) this.fail(asGridError(error));
421
+ }
422
+
423
+ /** Shows rows read on a server before the table is open. */
424
+ seed(initial) {
425
+ const {
426
+ table,
427
+ plan,
428
+ rows
429
+ } = initial;
430
+ if (planKey(plan) !== planKey(this.snapshot.document.plan ?? {})) return;
431
+ try {
432
+ const view = {
433
+ key: planKey(plan),
434
+ plan,
435
+ checked: checkPlan(plan, table.columns, planOptions(table.capabilities)),
436
+ revision: table.revision,
437
+ edits: rows.view.edits.version,
438
+ info: rows.view,
439
+ cache: new ViewCache(),
440
+ abort: new AbortController()
441
+ };
442
+ const block = decodeBlock(rows.block.bytes);
443
+ view.cache.receive(rows.start, rows.rowIds, block.columns.map(column => column.name), block.rows, rows.truncated);
444
+ this.current = view;
445
+ this.snapshot = {
446
+ ...this.snapshot,
447
+ table,
448
+ view: rows.view
449
+ };
450
+ this.snapshot = {
451
+ ...this.snapshot,
452
+ displayed: this.displayedOf(this.snapshot)
453
+ };
454
+ } catch (error) {
455
+ if (!(error instanceof PlanError)) throw error;
456
+ }
457
+ }
458
+
459
+ // ----- views -----
460
+
461
+ /**
462
+ * Reads the view of a plan while the view on screen stays, and shows it when its first rows arrive, with the
463
+ * session moved by `view.update` (§5.4; §4.2 of react.md).
464
+ */
465
+ showPlan(plan) {
466
+ const table = this.snapshot.table;
467
+ if (!table || !this.client || !this.opened) return;
468
+ const key = planKey(plan);
469
+ if (table.status === "loading" && !table.capabilities.partialViews && key !== planKey({})) {
470
+ this.next?.abort.abort();
471
+ this.next = null;
472
+ this.publish({
473
+ pending: {
474
+ plan
475
+ }
476
+ });
477
+ return;
478
+ }
479
+ const fresh = view => view !== null && !view.abort.signal.aborted && view.key === key && view.revision === table.revision && view.edits === table.edits.version;
480
+ if (this.next && !fresh(this.next)) {
481
+ this.next.abort.abort();
482
+ this.next = null;
483
+ }
484
+ if (fresh(this.current)) {
485
+ if (this.snapshot.pending) this.publish({
486
+ pending: null
487
+ });
488
+ return;
489
+ }
490
+ if (this.next) return;
491
+ if (this.current?.info?.status === "building") this.current.abort.abort();
492
+ let checked;
493
+ try {
494
+ checked = checkPlan(plan, table.columns, planOptions(table.capabilities));
495
+ } catch (error) {
496
+ if (!(error instanceof PlanError)) throw error;
497
+ if (this.snapshot.pending) this.publish({
498
+ pending: null
499
+ });
500
+ this.fail(asGridError(error));
501
+ return;
502
+ }
503
+ const view = {
504
+ key,
505
+ plan,
506
+ checked,
507
+ revision: table.revision,
508
+ edits: table.edits.version,
509
+ info: null,
510
+ cache: new ViewCache(),
511
+ abort: new AbortController()
512
+ };
513
+ this.next = view;
514
+ if (this.current) this.publish({
515
+ pending: {
516
+ plan
517
+ }
518
+ });
519
+ void this.prepare(view, this.connection);
520
+ }
521
+ live(view, connection) {
522
+ return connection === this.connection && view === this.next;
523
+ }
524
+ async prepare(view, connection) {
525
+ const table = this.snapshot.table;
526
+ const client = this.client;
527
+ const columns = this.reader.withKeyFor(view, this.reader.viewport.columns);
528
+ try {
529
+ const top = this.current ? this.reader.viewport.top : this.base("session").scroll?.row ?? 0;
530
+ await this.reader.read(view, this.reader.around(top, null, columns));
531
+ if (!this.live(view, connection) || !view.info) return;
532
+ let info = view.info;
533
+ if (top >= info.rowCount && info.rowCount > 0) {
534
+ await this.reader.read(view, this.reader.around(info.rowCount - 1, info.rowCount, columns));
535
+ if (!this.live(view, connection) || !view.info) return;
536
+ info = view.info;
537
+ }
538
+ // The session as it is now: the person may have moved while the rows were read.
539
+ const session = this.base("session");
540
+ const document = this.base("document");
541
+ const key = session.active?.key;
542
+ const located = [];
543
+ if (key && session.view !== info.hash && table.capabilities.locate) {
544
+ const result = await client.request("locate", {
545
+ table: table.table,
546
+ revision: view.revision,
547
+ plan: view.plan,
548
+ keys: [key],
549
+ ...(document.rowKey === undefined ? {} : {
550
+ rowKey: document.rowKey
551
+ })
552
+ }, view.abort.signal);
553
+ if (!this.live(view, connection)) return;
554
+ located.push({
555
+ key,
556
+ row: result.rows[0] ?? null
557
+ });
558
+ }
559
+ const state = {
560
+ document: this.base("document"),
561
+ session: this.base("session")
562
+ };
563
+ const updated = applyCommand(state, {
564
+ command: "view.update",
565
+ hash: info.hash,
566
+ located
567
+ }, this.contextFor(view, table, info));
568
+ const scroll = updated.session.scroll;
569
+ const target = scroll && scroll !== state.session.scroll ? scroll.row : Math.min(top, info.rowCount - 1);
570
+ if (info.status === "ready" && target >= 0 && view.cache.value(target, columns[0] ?? "") === undefined && columns.length > 0) {
571
+ await this.reader.read(view, this.reader.around(target, info.rowCount, columns));
572
+ if (!this.live(view, connection)) return;
573
+ }
574
+ const previous = this.current;
575
+ previous?.abort.abort();
576
+ this.current = view;
577
+ this.next = null;
578
+ // The rows, the view and the session's new positions are shown in one step.
579
+ // A view that comes on screen ends the failure of the one before (a pivot with too many columns).
580
+ this.change(updated.document, updated.session, {
581
+ view: view.info ?? info,
582
+ pending: null,
583
+ error: null
584
+ });
585
+ // The snapshot keeps the columns of the view before when they are the same, for commands to key on.
586
+ view.info = this.snapshot.view;
587
+ // Values of writes that the new rows hold are no longer needed.
588
+ this.dropWritten(write => (this.writeVersions.get(write) ?? Number.POSITIVE_INFINITY) <= view.edits);
589
+ this.emitRows();
590
+ this.reader.schedule();
591
+ // A normal read waits for completion, also when there is no event channel or the ready event raced
592
+ // the preview. It reports a failed build and is cancelled when the plan or connection changes.
593
+ if (view.info?.status === "building" && table.status === "ready") {
594
+ void this.reader.read(view, this.reader.around(Math.max(0, target), null, columns)).then(() => this.reader.schedule(), () => {});
595
+ }
596
+ } catch (error) {
597
+ if (view === this.next) {
598
+ this.next = null;
599
+ if (this.snapshot.pending) this.publish({
600
+ pending: null
601
+ });
602
+ }
603
+ if (connection === this.connection && !isCancelled(error)) this.fail(asGridError(error));
604
+ }
605
+ }
606
+ contextFor(view, table, info = view.info) {
607
+ const rowKey = this.base("document").rowKey;
608
+ return {
609
+ table: table.columns,
610
+ revision: view.revision,
611
+ view: {
612
+ columns: info?.columns ?? view.checked.columns,
613
+ rowCount: info?.rowCount ?? 0,
614
+ rowIds: view.checked.rowIds
615
+ },
616
+ lookup: row => this.withWritten(view.cache.loaded(row, rowKey)),
617
+ page: Math.max(1, this.reader.viewport.rows - 1),
618
+ capabilities: table.capabilities
619
+ };
620
+ }
621
+
622
+ // ----- changing the state -----
623
+
624
+ /** A row as commands read it, with the values of writes that its rows do not hold yet. */
625
+ withWritten(row) {
626
+ if (!row?.key || this.written.size === 0) return row;
627
+ const values = this.written.get(keyText(row.key));
628
+ if (!values) return row;
629
+ return {
630
+ key: row.key,
631
+ has: column => values.has(column) || row.has(column),
632
+ get: column => {
633
+ const entry = values.get(column);
634
+ return entry === undefined ? row.get(column) : entry.value;
635
+ }
636
+ };
637
+ }
638
+
639
+ /** Shows the values of a write until it is answered (§7.2). */
640
+ markWritten(write, updates) {
641
+ for (const update of updates) {
642
+ const text = keyText(update.key);
643
+ let values = this.written.get(text);
644
+ if (!values) {
645
+ values = new Map();
646
+ this.written.set(text, values);
647
+ }
648
+ values.set(update.column, {
649
+ value: update.value,
650
+ write
651
+ });
652
+ }
653
+ this.emitRows();
654
+ }
655
+
656
+ /** Forgets the values of writes: one that failed, or those that the rows on screen now hold. */
657
+ dropWritten(done) {
658
+ let dropped = false;
659
+ for (const [text, values] of this.written) {
660
+ for (const [column, entry] of values) {
661
+ if (!done(entry.write)) continue;
662
+ values.delete(column);
663
+ dropped = true;
664
+ }
665
+ if (values.size === 0) this.written.delete(text);
666
+ }
667
+ if (dropped) this.emitRows();
668
+ }
669
+
670
+ /** Makes a part controlled: commands report what they make of it, and only `setDocument`/`setSession` show it. */
671
+ control(part, controlled) {
672
+ this.controlled[part] = controlled;
673
+ if (!controlled) delete this.proposed[part];
674
+ }
675
+
676
+ /** Shows a document given by the host (a controlled prop, a saved view, an agent). */
677
+ setDocument(document) {
678
+ delete this.proposed.document;
679
+ const previous = this.snapshot.document;
680
+ if (document === previous) return;
681
+ this.publish({
682
+ document
683
+ });
684
+ if (document.plan !== previous.plan) this.showPlan(document.plan ?? {});
685
+ this.reader.schedule();
686
+ }
687
+
688
+ /** Shows a session given by the host. */
689
+ setSession(session) {
690
+ delete this.proposed.session;
691
+ if (session === this.snapshot.session) return;
692
+ this.publish({
693
+ session
694
+ });
695
+ }
696
+
697
+ /**
698
+ * Applies a new state from a command: shows what is not controlled, with `extra` members of the snapshot in the
699
+ * same step, and reports both parts that changed.
700
+ */
701
+ change(document, session, extra = {}) {
702
+ const shown = {
703
+ ...extra
704
+ };
705
+ const previous = this.snapshot.document;
706
+ const reports = [];
707
+ if (document !== this.base("document")) {
708
+ if (this.controlled.document) this.proposed.document = document;else shown.document = document;
709
+ reports.push(["document", document]);
710
+ }
711
+ if (session !== this.base("session")) {
712
+ if (this.controlled.session) this.proposed.session = session;else shown.session = session;
713
+ reports.push(["session", session]);
714
+ }
715
+ if (Object.keys(shown).length > 0) this.publish(shown);
716
+ for (const [part, value] of reports) {
717
+ if (part === "document") this.emit("document", value);else this.emit("session", value);
718
+ }
719
+ if (shown.document && shown.document.plan !== previous.plan) this.showPlan(shown.document.plan ?? {});
720
+ }
721
+
722
+ /** Applies a command now, or tells why it cannot be applied (rows to read first included). */
723
+ applyNow(command, options) {
724
+ const view = this.current;
725
+ const table = this.snapshot.table;
726
+ if (!view?.info || !table) {
727
+ return {
728
+ kind: "failed",
729
+ error: new GridError("not_ready", "The table is not open yet."),
730
+ view: null
731
+ };
732
+ }
733
+ const context = this.contextFor(view, table);
734
+ if (options.clipboard) context.clipboard = options.clipboard;
735
+ const started = now();
736
+ try {
737
+ const result = applyCommand({
738
+ document: this.base("document"),
739
+ session: this.base("session")
740
+ }, command, context);
741
+ track(command.command, started, "commands");
742
+ this.change(result.document, result.session);
743
+ return {
744
+ kind: "applied",
745
+ result,
746
+ table
747
+ };
748
+ } catch (error) {
749
+ if (!(error instanceof CommandError)) throw error;
750
+ track(command.command, started, "commands", "error");
751
+ return {
752
+ kind: "failed",
753
+ error,
754
+ view
755
+ };
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Applies a command (§4.2 of react.md). The state changes at once when the command has what it needs;
761
+ * a command that needs rows that are not read reads them first. Resolves once the command's requests are
762
+ * answered, and never rejects. The same function for the life of the store.
763
+ */
764
+ dispatch = async (command, options = {}) => {
765
+ // The state never shares objects with a command that its caller may change later.
766
+ const given = structuredClone(command);
767
+ // An undo or a redo takes the last step once the writes sent before it are in the history.
768
+ if (given.command === "history.undo" || given.command === "history.redo") await this.writing;
769
+ let applied = this.applyNow(given, options);
770
+ for (let tries = 0; tries < 3 && applied.kind === "failed" && applied.view && applied.error instanceof CommandError && applied.error.reason === "not_loaded" && applied.error.details; tries++) {
771
+ const view = applied.view;
772
+ try {
773
+ await this.reader.load(view, applied.error.details);
774
+ } catch (failure) {
775
+ // A view that another replaced while its rows were read (a write, or the file read again) is cancelled:
776
+ // the command applies to the new one.
777
+ if (!isCancelled(failure) || view === this.current) return {
778
+ ok: false,
779
+ error: asGridError(failure)
780
+ };
781
+ }
782
+ applied = this.applyNow(given, options);
783
+ }
784
+ if (applied.kind === "failed") return {
785
+ ok: false,
786
+ error: applied.error
787
+ };
788
+ try {
789
+ const output = await this.send(given.command, applied.result.requests, applied.table);
790
+ const value = output ?? applied.result.output;
791
+ return value === undefined ? {
792
+ ok: true
793
+ } : {
794
+ ok: true,
795
+ output: value
796
+ };
797
+ } catch (error) {
798
+ return {
799
+ ok: false,
800
+ error: asGridError(error)
801
+ };
802
+ }
803
+ };
804
+
805
+ /**
806
+ * Applies a command synchronously, as a `copy` event needs (its clipboard is written before the handler
807
+ * returns). A command that needs rows that are not read fails with `not_loaded`; its requests are sent.
808
+ */
809
+ dispatchSync(command, options = {}) {
810
+ const given = structuredClone(command);
811
+ const applied = this.applyNow(given, options);
812
+ if (applied.kind === "failed") return {
813
+ ok: false,
814
+ error: applied.error
815
+ };
816
+ if (applied.result.requests.length > 0) {
817
+ void this.send(given.command, applied.result.requests, applied.table).catch(error => {
818
+ this.fail(asGridError(error));
819
+ });
820
+ }
821
+ return applied.result.output === undefined ? {
822
+ ok: true
823
+ } : {
824
+ ok: true,
825
+ output: applied.result.output
826
+ };
827
+ }
828
+
829
+ /**
830
+ * Sends the failed write again (§6.6 of react.md): the same updates, or after an `edit_conflict` without their
831
+ * `previous`, so that they write over what the cells hold now. It succeeds as its command would have: the history
832
+ * gets its step, or an undo or a redo whose step is still the last moves it. Resolves as `dispatch` does.
833
+ */
834
+ retryWrite = async () => {
835
+ const failed = this.snapshot.failedWrite;
836
+ const table = this.snapshot.table;
837
+ if (!failed) return {
838
+ ok: false,
839
+ error: new GridError("invalid_request", "No write failed.")
840
+ };
841
+ if (failed.retry === null || !table || table.revision !== failed.revision) {
842
+ return {
843
+ ok: false,
844
+ error: new GridError("revision_changed", "The write cannot be sent again.")
845
+ };
846
+ }
847
+ this.publish({
848
+ failedWrite: null
849
+ });
850
+ const request = {
851
+ op: "write",
852
+ revision: failed.revision,
853
+ updates: failed.updates
854
+ };
855
+ if (failed.rowKey !== undefined) request.rowKey = failed.rowKey;
856
+ const command = this.stillLast(failed) ? failed.command : "write";
857
+ try {
858
+ await this.send(command, [request], table, failed.retry === "overwrite");
859
+ return {
860
+ ok: true
861
+ };
862
+ } catch (error) {
863
+ return {
864
+ ok: false,
865
+ error: asGridError(error)
866
+ };
867
+ }
868
+ };
869
+
870
+ /** Forgets the failed write: its cells show what the engine holds. */
871
+ discardWrite() {
872
+ if (this.snapshot.failedWrite) this.publish({
873
+ failedWrite: null
874
+ });
875
+ }
876
+
877
+ /** Keeps a write that failed, for `retryWrite` and `discardWrite`. */
878
+ failWrite(command, request, error) {
879
+ const failed = {
880
+ command,
881
+ updates: request.updates,
882
+ revision: request.revision ?? null,
883
+ ...(request.rowKey === undefined ? {} : {
884
+ rowKey: request.rowKey
885
+ }),
886
+ error,
887
+ retry: error.code === "edit_conflict" ? "overwrite" : RETRIED.has(error.code) ? "retry" : null
888
+ };
889
+ this.publish({
890
+ failedWrite: failed
891
+ });
892
+ }
893
+
894
+ /** Whether a failed undo or redo has its step still last in its list, where its write then moves it from. */
895
+ stillLast(failed) {
896
+ const list = failed.command === "history.undo" ? "undo" : failed.command === "history.redo" ? "redo" : null;
897
+ if (!list) return true;
898
+ const step = this.base("session").history?.[list].at(-1);
899
+ if (!step) return false;
900
+ const written = step.updates.map(update => ({
901
+ key: update.key,
902
+ column: update.column,
903
+ value: list === "undo" ? update.before : update.after,
904
+ previous: list === "undo" ? update.after : update.before
905
+ }));
906
+ const sent = failed.updates.map(({
907
+ key,
908
+ column,
909
+ value,
910
+ previous
911
+ }) => ({
912
+ key,
913
+ column,
914
+ value,
915
+ previous
916
+ }));
917
+ return canonicalJson(written) === canonicalJson(sent);
918
+ }
919
+
920
+ /** Where the file of an `export` without a target can be downloaded (`Transport.exportUrl`), or null. */
921
+ exportUrl(target) {
922
+ return this.options.transport.exportUrl?.(target) ?? null;
923
+ }
924
+
925
+ /** A name for the file of an export that an engine answers as data: the table's name with the format's extension. */
926
+ exportName(format) {
927
+ const name = this.options.table.split(/[\\/]/).pop() ?? "";
928
+ const base = name.replace(/(\.(gz|zst))?$/i, "").replace(/\.[^.]*$/, "") || "export";
929
+ return `${base}.${format}`;
930
+ }
931
+
932
+ /**
933
+ * Reads the table's file as it is now (`reload`, §7.6), without the cells written into the table as it was read:
934
+ * for a file that changed on disk while the table had written cells (`fileChanged` of the table).
935
+ */
936
+ async reload() {
937
+ const table = this.snapshot.table;
938
+ const client = this.client;
939
+ const connection = this.connection;
940
+ if (!table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
941
+ const info = await client.request("reload", {
942
+ table: table.table
943
+ });
944
+ if (connection === this.connection) this.adopt(info);
945
+ }
946
+
947
+ /** Another revision of the table (§5.4): the document is repaired for its columns (§5.5), and the view read. */
948
+ adopt(info) {
949
+ const current = this.snapshot.table;
950
+ if (current && current.revision === info.revision && current.edits.version === info.edits.version && current.fileChanged === info.fileChanged && current.status === info.status) {
951
+ return;
952
+ }
953
+ const repaired = repairDocument(this.base("document"), info.columns, planOptions(info.capabilities));
954
+ this.publish({
955
+ table: info,
956
+ status: info.status === "error" ? "error" : "ready"
957
+ });
958
+ if (repaired.removed.length > 0) this.change(repaired.document, this.base("session"));
959
+ this.showPlan(this.base("document").plan ?? {});
960
+ this.reader.schedule();
961
+ }
962
+
963
+ /** Counts and sums the cells of ranges of the view on screen (`summarize`, §8.2). */
964
+ async summarize(ranges, columns, signal) {
965
+ const view = this.current;
966
+ const table = this.snapshot.table;
967
+ const client = this.client;
968
+ if (!view || !table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
969
+ return client.request("summarize", {
970
+ table: table.table,
971
+ revision: view.revision,
972
+ plan: view.plan,
973
+ ranges,
974
+ columns
975
+ }, signal);
976
+ }
977
+
978
+ /**
979
+ * The values of a column with how many rows have each, the most common first, for the column menu's list: the
980
+ * rows of the document's plan without the column's own conditions, and those whose text contains `search`
981
+ * (`valuesPlan`). At most `limit` of them; `truncated` when there are more.
982
+ */
983
+ async values(column, {
984
+ search = "",
985
+ limit = 1_000
986
+ } = {}, signal) {
987
+ const table = this.snapshot.table;
988
+ const client = this.client;
989
+ if (!table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
990
+ const made = valuesPlan(this.base("document").plan ?? {}, column, search);
991
+ if (!made) throw new GridError("invalid_plan", "The values of a grouped view are not listed.");
992
+ const result = await client.request("rows", {
993
+ table: table.table,
994
+ revision: this.current?.revision ?? table.revision,
995
+ plan: made.plan,
996
+ start: 0,
997
+ end: limit + 1,
998
+ columns: [column, made.count]
999
+ }, signal);
1000
+ const rows = decodeBlock(result.block.bytes).rows;
1001
+ return {
1002
+ values: rows.slice(0, limit).map(([value, count]) => ({
1003
+ value,
1004
+ count: Number(count)
1005
+ })),
1006
+ truncated: rows.length > limit
1007
+ };
1008
+ }
1009
+
1010
+ /**
1011
+ * Finds the next cell of the view on screen after `from` (before it with `direction` −1) whose text contains
1012
+ * `text` (`find`, §8.2), among the displayed columns in display order, wrapping around; null when none does.
1013
+ */
1014
+ async find(text, {
1015
+ from,
1016
+ direction = 1,
1017
+ caseSensitive = false
1018
+ }, signal) {
1019
+ const view = this.current;
1020
+ const table = this.snapshot.table;
1021
+ const client = this.client;
1022
+ if (!view || !table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
1023
+ const columns = this.snapshot.displayed.map(item => item.column.name);
1024
+ const column = Math.max(0, columns.indexOf(from.column));
1025
+ const found = await client.request("find", {
1026
+ table: table.table,
1027
+ revision: view.revision,
1028
+ plan: view.plan,
1029
+ text,
1030
+ columns,
1031
+ from: {
1032
+ row: from.row,
1033
+ column
1034
+ },
1035
+ direction,
1036
+ ...(caseSensitive ? {
1037
+ caseSensitive: true
1038
+ } : {})
1039
+ }, signal);
1040
+ return found ? {
1041
+ row: found.row,
1042
+ column: columns[found.column]
1043
+ } : null;
1044
+ }
1045
+
1046
+ /** The whole value of a cell of the view on screen, also when the engine cut its text (§8.4), and its type. */
1047
+ async fullValue(row, column, signal) {
1048
+ const view = this.current;
1049
+ const table = this.snapshot.table;
1050
+ const client = this.client;
1051
+ if (!view || !table || !client || !this.opened) throw new GridError("not_ready", "The table is not open yet.");
1052
+ const result = await client.request("rows", {
1053
+ table: table.table,
1054
+ revision: view.revision,
1055
+ plan: view.plan,
1056
+ start: row,
1057
+ end: row + 1,
1058
+ columns: [column],
1059
+ textLimit: 1_000_000
1060
+ }, signal);
1061
+ const block = decodeBlock(result.block.bytes);
1062
+ return {
1063
+ value: block.rows[0]?.[0] ?? null,
1064
+ column: block.columns[0]
1065
+ };
1066
+ }
1067
+
1068
+ /** Sends the requests of a command and applies the commands of §6.7 that their answers call for. */
1069
+ async send(name, requests, table, overwrite = false) {
1070
+ if (!requests.some(request => request.op === "write")) return this.sendNow(name, requests, table, overwrite);
1071
+ let done = () => {};
1072
+ const finished = new Promise(resolve => {
1073
+ done = resolve;
1074
+ });
1075
+ this.writing = this.writing.then(() => finished);
1076
+ try {
1077
+ return await this.sendNow(name, requests, table, overwrite);
1078
+ } finally {
1079
+ done();
1080
+ }
1081
+ }
1082
+ async sendNow(name, requests, table, overwrite) {
1083
+ const client = this.client;
1084
+ let output;
1085
+ for (const request of requests) {
1086
+ if (!client) throw new GridError("not_ready", "The grid is not connected.");
1087
+ const {
1088
+ op,
1089
+ ...members
1090
+ } = request;
1091
+ const updates = op === "write" ? members.updates : [];
1092
+ const write = op === "write" ? ++this.writes : 0;
1093
+ if (write) this.markWritten(write, updates);
1094
+ // An overwrite checks no `previous`; the history keeps them all the same, as the values the person wrote over.
1095
+ const sent = overwrite ? {
1096
+ ...members,
1097
+ updates: updates.map(({
1098
+ previous: _,
1099
+ ...update
1100
+ }) => update)
1101
+ } : members;
1102
+ let result;
1103
+ try {
1104
+ result = await client.request(op, {
1105
+ table: table.table,
1106
+ ...sent
1107
+ });
1108
+ } catch (error) {
1109
+ if (write) {
1110
+ this.dropWritten(other => other === write);
1111
+ if (client === this.client) this.failWrite(name, request, asGridError(error));
1112
+ }
1113
+ throw error;
1114
+ }
1115
+ if (op === "write") {
1116
+ const core = name === "history.undo" ? {
1117
+ command: "history.undone"
1118
+ } : name === "history.redo" ? {
1119
+ command: "history.redone"
1120
+ } : {
1121
+ command: "history.push",
1122
+ updates: updates.map(update => ({
1123
+ key: update.key,
1124
+ column: update.column,
1125
+ before: update.previous ?? null,
1126
+ after: update.value
1127
+ }))
1128
+ };
1129
+ const state = {
1130
+ document: this.base("document"),
1131
+ session: this.base("session")
1132
+ };
1133
+ const view = this.current;
1134
+ if (view) {
1135
+ const applied = applyCommand(state, core, this.contextFor(view, table));
1136
+ this.change(applied.document, applied.session);
1137
+ }
1138
+ const edits = result.edits;
1139
+ this.writeVersions.set(write, edits.version);
1140
+ // The engine's event may have brought the view of this version on screen before the answer came.
1141
+ if (this.current && this.current.edits >= edits.version) this.dropWritten(other => other === write);
1142
+ const current = this.snapshot.table;
1143
+ if (current) this.publish({
1144
+ table: {
1145
+ ...current,
1146
+ edits
1147
+ }
1148
+ });
1149
+ this.showPlan(this.base("document").plan ?? {});
1150
+ } else {
1151
+ output = result;
1152
+ }
1153
+ }
1154
+ return output;
1155
+ }
1156
+ }
1157
+
1158
+ /** Makes a store (§4 of react.md). */
1159
+ export function createGridStore(options) {
1160
+ return new GridStore(options);
1161
+ }
1162
+ //# sourceMappingURL=store.js.map