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.
- package/README.md +175 -13
- package/dist/canvas/geometry.d.ts +92 -0
- package/dist/canvas/geometry.d.ts.map +1 -0
- package/dist/canvas/geometry.js +197 -0
- package/dist/canvas/geometry.js.map +1 -0
- package/dist/canvas/grid-canvas.d.ts +161 -0
- package/dist/canvas/grid-canvas.d.ts.map +1 -0
- package/dist/canvas/grid-canvas.js +1146 -0
- package/dist/canvas/grid-canvas.js.map +1 -0
- package/dist/canvas/hooks.d.ts +61 -0
- package/dist/canvas/hooks.d.ts.map +1 -0
- package/dist/canvas/hooks.js +2 -0
- package/dist/canvas/hooks.js.map +1 -0
- package/dist/canvas/keys.d.ts +26 -0
- package/dist/canvas/keys.d.ts.map +1 -0
- package/dist/canvas/keys.js +133 -0
- package/dist/canvas/keys.js.map +1 -0
- package/dist/canvas/layout.d.ts +26 -0
- package/dist/canvas/layout.d.ts.map +1 -0
- package/dist/canvas/layout.js +73 -0
- package/dist/canvas/layout.js.map +1 -0
- package/dist/canvas/scroll.d.ts +31 -0
- package/dist/canvas/scroll.d.ts.map +1 -0
- package/dist/canvas/scroll.js +111 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/text.d.ts +23 -0
- package/dist/canvas/text.d.ts.map +1 -0
- package/dist/canvas/text.js +142 -0
- package/dist/canvas/text.js.map +1 -0
- package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
- package/dist/canvas/theme.d.ts.map +1 -0
- package/dist/canvas/theme.js +62 -0
- package/dist/canvas/theme.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +222 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checked.d.ts +20 -0
- package/dist/core/checked.d.ts.map +1 -0
- package/dist/core/checked.js +61 -0
- package/dist/core/checked.js.map +1 -0
- package/dist/core/client.d.ts +33 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +103 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/conditions.d.ts +48 -0
- package/dist/core/conditions.d.ts.map +1 -0
- package/dist/core/conditions.js +105 -0
- package/dist/core/conditions.js.map +1 -0
- package/dist/core/evaluate.d.ts +20 -0
- package/dist/core/evaluate.d.ts.map +1 -0
- package/dist/core/evaluate.js +210 -0
- package/dist/core/evaluate.js.map +1 -0
- package/dist/core/filters.d.ts +51 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +303 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/format.d.ts +14 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +303 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/measures.d.ts +26 -0
- package/dist/core/measures.d.ts.map +1 -0
- package/dist/core/measures.js +105 -0
- package/dist/core/measures.js.map +1 -0
- package/dist/core/request-ids.d.ts +20 -0
- package/dist/core/request-ids.d.ts.map +1 -0
- package/dist/core/request-ids.js +55 -0
- package/dist/core/request-ids.js.map +1 -0
- package/dist/core/rows.d.ts +79 -0
- package/dist/core/rows.d.ts.map +1 -0
- package/dist/core/rows.js +173 -0
- package/dist/core/rows.js.map +1 -0
- package/dist/core/store.d.ts +233 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +1162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/totals.d.ts +24 -0
- package/dist/core/totals.d.ts.map +1 -0
- package/dist/core/totals.js +49 -0
- package/dist/core/totals.js.map +1 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transports.d.ts +50 -0
- package/dist/core/transports.d.ts.map +1 -0
- package/dist/core/transports.js +160 -0
- package/dist/core/transports.js.map +1 -0
- package/dist/core/types.d.ts +150 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +45 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +11 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/react/accessible-rows.d.ts +13 -0
- package/dist/react/accessible-rows.d.ts.map +1 -0
- package/dist/react/accessible-rows.js +320 -0
- package/dist/react/accessible-rows.js.map +1 -0
- package/dist/react/cell-editor.d.ts +22 -0
- package/dist/react/cell-editor.d.ts.map +1 -0
- package/dist/react/cell-editor.js +431 -0
- package/dist/react/cell-editor.js.map +1 -0
- package/dist/react/cell-menu.d.ts +18 -0
- package/dist/react/cell-menu.d.ts.map +1 -0
- package/dist/react/cell-menu.js +299 -0
- package/dist/react/cell-menu.js.map +1 -0
- package/dist/react/column-menu.d.ts +16 -0
- package/dist/react/column-menu.d.ts.map +1 -0
- package/dist/react/column-menu.js +1311 -0
- package/dist/react/column-menu.js.map +1 -0
- package/dist/react/columns-menu.d.ts +7 -0
- package/dist/react/columns-menu.d.ts.map +1 -0
- package/dist/react/columns-menu.js +249 -0
- package/dist/react/columns-menu.js.map +1 -0
- package/dist/react/conditions-menu.d.ts +10 -0
- package/dist/react/conditions-menu.d.ts.map +1 -0
- package/dist/react/conditions-menu.js +795 -0
- package/dist/react/conditions-menu.js.map +1 -0
- package/dist/react/context.d.ts +29 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +18 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/data-grid.d.ts +79 -0
- package/dist/react/data-grid.d.ts.map +1 -0
- package/dist/react/data-grid.js +653 -0
- package/dist/react/data-grid.js.map +1 -0
- package/dist/react/detail-panel.d.ts +22 -0
- package/dist/react/detail-panel.d.ts.map +1 -0
- package/dist/react/detail-panel.js +242 -0
- package/dist/react/detail-panel.js.map +1 -0
- package/dist/react/export-menu.d.ts +11 -0
- package/dist/react/export-menu.d.ts.map +1 -0
- package/dist/react/export-menu.js +304 -0
- package/dist/react/export-menu.js.map +1 -0
- package/dist/react/find-bar.d.ts +11 -0
- package/dist/react/find-bar.d.ts.map +1 -0
- package/dist/react/find-bar.js +303 -0
- package/dist/react/find-bar.js.map +1 -0
- package/dist/react/format-form.d.ts +12 -0
- package/dist/react/format-form.d.ts.map +1 -0
- package/dist/react/format-form.js +863 -0
- package/dist/react/format-form.js.map +1 -0
- package/dist/react/formula-menu.d.ts +6 -0
- package/dist/react/formula-menu.d.ts.map +1 -0
- package/dist/react/formula-menu.js +343 -0
- package/dist/react/formula-menu.js.map +1 -0
- package/dist/react/grid-body.d.ts +33 -0
- package/dist/react/grid-body.d.ts.map +1 -0
- package/dist/react/grid-body.js +1389 -0
- package/dist/react/grid-body.js.map +1 -0
- package/dist/react/group-menu.d.ts +7 -0
- package/dist/react/group-menu.d.ts.map +1 -0
- package/dist/react/group-menu.js +508 -0
- package/dist/react/group-menu.js.map +1 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +161 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/messages.d.ts +180 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +414 -0
- package/dist/react/messages.js.map +1 -0
- package/dist/react/save.d.ts +9 -0
- package/dist/react/save.d.ts.map +1 -0
- package/dist/react/save.js +102 -0
- package/dist/react/save.js.map +1 -0
- package/dist/react/static-rows.d.ts +9 -0
- package/dist/react/static-rows.d.ts.map +1 -0
- package/dist/react/static-rows.js +176 -0
- package/dist/react/static-rows.js.map +1 -0
- package/dist/react/status-bar.d.ts +12 -0
- package/dist/react/status-bar.d.ts.map +1 -0
- package/dist/react/status-bar.js +413 -0
- package/dist/react/status-bar.js.map +1 -0
- package/dist/react/toolbar-popover.d.ts +14 -0
- package/dist/react/toolbar-popover.d.ts.map +1 -0
- package/dist/react/toolbar-popover.js +143 -0
- package/dist/react/toolbar-popover.js.map +1 -0
- package/dist/react/toolbar.d.ts +13 -0
- package/dist/react/toolbar.d.ts.map +1 -0
- package/dist/react/toolbar.js +344 -0
- package/dist/react/toolbar.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +496 -316
- package/package.json +43 -18
- package/src/canvas/geometry.ts +197 -0
- package/src/canvas/grid-canvas.ts +1222 -0
- package/src/canvas/hooks.ts +61 -0
- package/src/canvas/keys.ts +96 -0
- package/src/canvas/layout.ts +77 -0
- package/src/canvas/scroll.ts +120 -0
- package/src/canvas/text.ts +155 -0
- package/src/{theme.ts → canvas/theme.ts} +14 -1
- package/src/core/cache.ts +247 -0
- package/src/core/checked.ts +64 -0
- package/src/core/client.ts +103 -0
- package/src/core/conditions.ts +89 -0
- package/src/core/evaluate.ts +221 -0
- package/src/core/filters.ts +268 -0
- package/src/core/format.ts +309 -0
- package/src/core/measures.ts +99 -0
- package/src/core/request-ids.ts +54 -0
- package/src/core/rows.ts +213 -0
- package/src/core/store.ts +1122 -0
- package/src/core/totals.ts +59 -0
- package/src/core/trace.ts +44 -0
- package/src/core/transports.ts +177 -0
- package/src/core/types.ts +168 -0
- package/src/core.ts +39 -0
- package/src/index.ts +24 -12
- package/src/react/accessible-rows.tsx +242 -0
- package/src/react/cell-editor.tsx +216 -0
- package/src/react/cell-menu.tsx +166 -0
- package/src/react/column-menu.tsx +619 -0
- package/src/react/columns-menu.tsx +116 -0
- package/src/react/conditions-menu.tsx +289 -0
- package/src/react/context.ts +33 -0
- package/src/react/data-grid.tsx +286 -0
- package/src/react/detail-panel.tsx +92 -0
- package/src/react/export-menu.tsx +112 -0
- package/src/react/find-bar.tsx +112 -0
- package/src/react/format-form.tsx +421 -0
- package/src/react/formula-menu.tsx +163 -0
- package/src/react/grid-body.tsx +744 -0
- package/src/react/group-menu.tsx +214 -0
- package/src/react/hooks.ts +104 -0
- package/src/react/messages.ts +556 -0
- package/src/react/save.ts +42 -0
- package/src/react/static-rows.tsx +59 -0
- package/src/react/status-bar.tsx +175 -0
- package/src/react/toolbar-popover.tsx +69 -0
- package/src/react/toolbar.tsx +152 -0
- package/src/server.ts +58 -0
- package/src/styles.css +496 -316
- package/dist/components/bars.d.ts +0 -14
- package/dist/components/bars.d.ts.map +0 -1
- package/dist/components/bars.js +0 -67
- package/dist/components/bars.js.map +0 -1
- package/dist/components/cell-inspector.d.ts +0 -11
- package/dist/components/cell-inspector.d.ts.map +0 -1
- package/dist/components/cell-inspector.js +0 -37
- package/dist/components/cell-inspector.js.map +0 -1
- package/dist/components/columns-panel.d.ts +0 -12
- package/dist/components/columns-panel.d.ts.map +0 -1
- package/dist/components/columns-panel.js +0 -11
- package/dist/components/columns-panel.js.map +0 -1
- package/dist/components/filter-panel.d.ts +0 -17
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -165
- package/dist/components/filter-panel.js.map +0 -1
- package/dist/components/find-bar.d.ts +0 -9
- package/dist/components/find-bar.d.ts.map +0 -1
- package/dist/components/find-bar.js +0 -46
- package/dist/components/find-bar.js.map +0 -1
- package/dist/components/popover.d.ts +0 -38
- package/dist/components/popover.d.ts.map +0 -1
- package/dist/components/popover.js +0 -104
- package/dist/components/popover.js.map +0 -1
- package/dist/components/stats-panel.d.ts +0 -12
- package/dist/components/stats-panel.d.ts.map +0 -1
- package/dist/components/stats-panel.js +0 -40
- package/dist/components/stats-panel.js.map +0 -1
- package/dist/controller.d.ts +0 -218
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -788
- package/dist/controller.js.map +0 -1
- package/dist/data-grid.d.ts +0 -51
- package/dist/data-grid.d.ts.map +0 -1
- package/dist/data-grid.js +0 -698
- package/dist/data-grid.js.map +0 -1
- package/dist/labels.d.ts +0 -9
- package/dist/labels.d.ts.map +0 -1
- package/dist/labels.js +0 -38
- package/dist/labels.js.map +0 -1
- package/dist/messages.d.ts +0 -99
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js +0 -276
- package/dist/messages.js.map +0 -1
- package/dist/renderer.d.ts +0 -52
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -522
- package/dist/renderer.js.map +0 -1
- package/dist/text.d.ts +0 -14
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js +0 -63
- package/dist/text.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -54
- package/dist/theme.js.map +0 -1
- package/src/components/bars.tsx +0 -179
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -290
- package/src/components/find-bar.tsx +0 -104
- package/src/components/popover.tsx +0 -194
- package/src/components/stats-panel.tsx +0 -153
- package/src/controller.ts +0 -887
- package/src/data-grid.tsx +0 -1029
- package/src/labels.ts +0 -46
- package/src/messages.ts +0 -390
- package/src/renderer.ts +0 -535
- 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
|