vastgrid 0.1.1 → 0.2.1
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 +151 -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 +42 -17
- 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 +169 -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 -68
- 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 -22
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -169
- 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 -227
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -845
- 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 -704
- 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 -180
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -293
- 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 -945
- package/src/data-grid.tsx +0 -1035
- 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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How cells show their values with the `number` and `date` of a column's format (§5.1 of the specification): 0.1's
|
|
3
|
+
* `NumberFormat` and `DateFormat`, ported from `vastgrid-core` (Intl formats, with the exact digits of decimals,
|
|
4
|
+
* and date patterns such as `yyyy/MM/dd`).
|
|
5
|
+
*/
|
|
6
|
+
import type { Column, ColumnFormat, DateFormat, JsonValue, NumberFormat } from "vastgrid-spec";
|
|
7
|
+
|
|
8
|
+
const decimalLiteral = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A value as input for `Intl.NumberFormat`, or null when it is not a finite number. Numeric strings
|
|
12
|
+
* stay strings, which `Intl.NumberFormat` formats from their exact digits.
|
|
13
|
+
*/
|
|
14
|
+
function numberInput(value: JsonValue): number | string | null {
|
|
15
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
16
|
+
if (typeof value !== "string") return null;
|
|
17
|
+
const text = value.trim();
|
|
18
|
+
return decimalLiteral.test(text) ? text : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function numberFormatter(locale: string | undefined, format: NumberFormat, grouping: boolean) {
|
|
22
|
+
const style = format.style ?? "decimal";
|
|
23
|
+
if (style === "currency" && !format.currency) throw new RangeError("A currency format needs `currency`.");
|
|
24
|
+
const options: Intl.NumberFormatOptions = {
|
|
25
|
+
style,
|
|
26
|
+
useGrouping: format.grouping ?? grouping,
|
|
27
|
+
minimumFractionDigits: format.fractionDigits ?? format.minimumFractionDigits,
|
|
28
|
+
maximumFractionDigits: format.fractionDigits ?? format.maximumFractionDigits,
|
|
29
|
+
};
|
|
30
|
+
if (style === "currency") {
|
|
31
|
+
options.currency = format.currency;
|
|
32
|
+
if (format.currencyDisplay) options.currencyDisplay = format.currencyDisplay;
|
|
33
|
+
}
|
|
34
|
+
if (format.notation) options.notation = format.notation;
|
|
35
|
+
if (format.signDisplay) options.signDisplay = format.signDisplay;
|
|
36
|
+
// A maximum below the style's default minimum (2 for USD, 0 for percent) lowers the minimum too.
|
|
37
|
+
if (options.maximumFractionDigits !== undefined && options.minimumFractionDigits === undefined) {
|
|
38
|
+
options.minimumFractionDigits = Math.min(
|
|
39
|
+
options.maximumFractionDigits,
|
|
40
|
+
new Intl.NumberFormat(locale, { ...options, maximumFractionDigits: undefined }).resolvedOptions()
|
|
41
|
+
.minimumFractionDigits ?? 0,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const intl = new Intl.NumberFormat(locale, options);
|
|
45
|
+
const prefix = format.prefix ?? "";
|
|
46
|
+
const suffix = format.suffix ?? "";
|
|
47
|
+
// `format` takes exact decimal strings (Intl.NumberFormat v3); the types only list numbers.
|
|
48
|
+
return (value: number | string) => `${prefix}${intl.format(value as number)}${suffix}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The parts of an ISO-like date, time or timestamp. */
|
|
52
|
+
interface DateParts {
|
|
53
|
+
year: number;
|
|
54
|
+
month: number;
|
|
55
|
+
day: number;
|
|
56
|
+
hour: number;
|
|
57
|
+
minute: number;
|
|
58
|
+
second: number;
|
|
59
|
+
/** Fraction digits of the second, as written. */
|
|
60
|
+
fraction: string;
|
|
61
|
+
hasDate: boolean;
|
|
62
|
+
hasTime: boolean;
|
|
63
|
+
/** Offset from UTC in minutes, for values with one. */
|
|
64
|
+
offset: number | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isoDateTime =
|
|
68
|
+
/^([+-]?\d{4,})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
|
|
69
|
+
const isoTime = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/;
|
|
70
|
+
|
|
71
|
+
function parseOffset(text: string | undefined) {
|
|
72
|
+
if (!text) return null;
|
|
73
|
+
if (text.toUpperCase() === "Z") return 0;
|
|
74
|
+
const sign = text.startsWith("-") ? -1 : 1;
|
|
75
|
+
const digits = text.slice(1).replace(":", "");
|
|
76
|
+
return sign * (Number(digits.slice(0, 2)) * 60 + Number(digits.slice(2) || 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseDate(text: string): DateParts | null {
|
|
80
|
+
const date = isoDateTime.exec(text.trim());
|
|
81
|
+
if (date) {
|
|
82
|
+
const [, year, month, day, hour, minute, second, fraction, offset] = date;
|
|
83
|
+
return {
|
|
84
|
+
year: Number(year),
|
|
85
|
+
month: Number(month),
|
|
86
|
+
day: Number(day),
|
|
87
|
+
hour: Number(hour ?? 0),
|
|
88
|
+
minute: Number(minute ?? 0),
|
|
89
|
+
second: Number(second ?? 0),
|
|
90
|
+
fraction: fraction ?? "",
|
|
91
|
+
hasDate: true,
|
|
92
|
+
hasTime: hour !== undefined,
|
|
93
|
+
offset: parseOffset(offset),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const time = isoTime.exec(text.trim());
|
|
97
|
+
if (!time) return null;
|
|
98
|
+
const [, hour, minute, second, fraction] = time;
|
|
99
|
+
return {
|
|
100
|
+
year: 1970,
|
|
101
|
+
month: 1,
|
|
102
|
+
day: 1,
|
|
103
|
+
hour: Number(hour),
|
|
104
|
+
minute: Number(minute),
|
|
105
|
+
second: Number(second ?? 0),
|
|
106
|
+
fraction: fraction ?? "",
|
|
107
|
+
hasDate: false,
|
|
108
|
+
hasTime: true,
|
|
109
|
+
offset: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The UTC time of the parts' wall clock (ignoring their offset). Years below 100 are kept. */
|
|
114
|
+
function wallClock(parts: DateParts) {
|
|
115
|
+
const date = new Date(0);
|
|
116
|
+
date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
|
|
117
|
+
date.setUTCHours(parts.hour, parts.minute, parts.second, Number(parts.fraction.slice(0, 3).padEnd(3, "0")));
|
|
118
|
+
return date;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const patternToken = /'(?:[^']|'')*'|yyyy|yy|MMMM|MMM|MM|M|dd|d|EEEE|EEE|HH|H|hh|h|mm|m|ss|s|S+|a/g;
|
|
122
|
+
|
|
123
|
+
function dateFormatter(locale: string | undefined, format: DateFormat) {
|
|
124
|
+
const { pattern, timeZone, ...options } = format;
|
|
125
|
+
// Checks the time zone and the options now, not on the first value.
|
|
126
|
+
new Intl.DateTimeFormat(locale, { ...options, timeZone });
|
|
127
|
+
const custom = Object.keys(options).length > 0;
|
|
128
|
+
const formats = new Map<string, Intl.DateTimeFormat>();
|
|
129
|
+
const intl = (key: string, make: () => Intl.DateTimeFormat) => {
|
|
130
|
+
let item = formats.get(key);
|
|
131
|
+
if (!item) {
|
|
132
|
+
item = make();
|
|
133
|
+
formats.set(key, item);
|
|
134
|
+
}
|
|
135
|
+
return item;
|
|
136
|
+
};
|
|
137
|
+
/** The parts in the shown time zone: values with an offset move to `timeZone`. */
|
|
138
|
+
const local = (parts: DateParts): DateParts => {
|
|
139
|
+
if (parts.offset === null) return parts;
|
|
140
|
+
const instant = new Date(wallClock(parts).getTime() - parts.offset * 60_000);
|
|
141
|
+
const fields = intl(
|
|
142
|
+
"parts",
|
|
143
|
+
() =>
|
|
144
|
+
new Intl.DateTimeFormat("en-US-u-nu-latn", {
|
|
145
|
+
timeZone,
|
|
146
|
+
year: "numeric",
|
|
147
|
+
month: "numeric",
|
|
148
|
+
day: "numeric",
|
|
149
|
+
hour: "numeric",
|
|
150
|
+
minute: "numeric",
|
|
151
|
+
second: "numeric",
|
|
152
|
+
hourCycle: "h23",
|
|
153
|
+
era: "short",
|
|
154
|
+
}),
|
|
155
|
+
).formatToParts(instant);
|
|
156
|
+
const field = (type: string) => Number(fields.find((part) => part.type === type)?.value ?? 0);
|
|
157
|
+
const bc = fields.find((part) => part.type === "era")?.value.startsWith("B");
|
|
158
|
+
return {
|
|
159
|
+
...parts,
|
|
160
|
+
year: bc ? 1 - field("year") : field("year"),
|
|
161
|
+
month: field("month"),
|
|
162
|
+
day: field("day"),
|
|
163
|
+
hour: field("hour"),
|
|
164
|
+
minute: field("minute"),
|
|
165
|
+
second: field("second"),
|
|
166
|
+
offset: null,
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (pattern !== undefined) {
|
|
171
|
+
const tokens = pattern.match(patternToken) ?? [];
|
|
172
|
+
const texts = pattern.split(patternToken);
|
|
173
|
+
const name = (key: string, options: Intl.DateTimeFormatOptions, date: Date) =>
|
|
174
|
+
intl(key, () => new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" })).format(date);
|
|
175
|
+
const pad = (value: number, length: number) =>
|
|
176
|
+
value < 0 ? `-${String(-value).padStart(length, "0")}` : String(value).padStart(length, "0");
|
|
177
|
+
return (parts: DateParts) => {
|
|
178
|
+
const shown = local(parts);
|
|
179
|
+
const date = wallClock(shown);
|
|
180
|
+
let text = texts[0];
|
|
181
|
+
tokens.forEach((token, index) => {
|
|
182
|
+
let part: string;
|
|
183
|
+
switch (token) {
|
|
184
|
+
case "yyyy":
|
|
185
|
+
part = pad(shown.year, 4);
|
|
186
|
+
break;
|
|
187
|
+
case "yy":
|
|
188
|
+
part = pad(Math.abs(shown.year) % 100, 2);
|
|
189
|
+
break;
|
|
190
|
+
case "MMMM":
|
|
191
|
+
part = name("MMMM", { month: "long" }, date);
|
|
192
|
+
break;
|
|
193
|
+
case "MMM":
|
|
194
|
+
part = name("MMM", { month: "short" }, date);
|
|
195
|
+
break;
|
|
196
|
+
case "MM":
|
|
197
|
+
part = pad(shown.month, 2);
|
|
198
|
+
break;
|
|
199
|
+
case "M":
|
|
200
|
+
part = String(shown.month);
|
|
201
|
+
break;
|
|
202
|
+
case "dd":
|
|
203
|
+
part = pad(shown.day, 2);
|
|
204
|
+
break;
|
|
205
|
+
case "d":
|
|
206
|
+
part = String(shown.day);
|
|
207
|
+
break;
|
|
208
|
+
case "EEEE":
|
|
209
|
+
part = name("EEEE", { weekday: "long" }, date);
|
|
210
|
+
break;
|
|
211
|
+
case "EEE":
|
|
212
|
+
part = name("EEE", { weekday: "short" }, date);
|
|
213
|
+
break;
|
|
214
|
+
case "HH":
|
|
215
|
+
part = pad(shown.hour, 2);
|
|
216
|
+
break;
|
|
217
|
+
case "H":
|
|
218
|
+
part = String(shown.hour);
|
|
219
|
+
break;
|
|
220
|
+
case "hh":
|
|
221
|
+
part = pad(shown.hour % 12 || 12, 2);
|
|
222
|
+
break;
|
|
223
|
+
case "h":
|
|
224
|
+
part = String(shown.hour % 12 || 12);
|
|
225
|
+
break;
|
|
226
|
+
case "a":
|
|
227
|
+
part =
|
|
228
|
+
intl("a", () => new Intl.DateTimeFormat(locale, { hour: "numeric", hour12: true, timeZone: "UTC" }))
|
|
229
|
+
.formatToParts(date)
|
|
230
|
+
.find((item) => item.type === "dayPeriod")?.value ?? (shown.hour < 12 ? "AM" : "PM");
|
|
231
|
+
break;
|
|
232
|
+
case "mm":
|
|
233
|
+
part = pad(shown.minute, 2);
|
|
234
|
+
break;
|
|
235
|
+
case "m":
|
|
236
|
+
part = String(shown.minute);
|
|
237
|
+
break;
|
|
238
|
+
case "ss":
|
|
239
|
+
part = pad(shown.second, 2);
|
|
240
|
+
break;
|
|
241
|
+
case "s":
|
|
242
|
+
part = String(shown.second);
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
part = token.startsWith("S")
|
|
246
|
+
? shown.fraction.slice(0, token.length).padEnd(token.length, "0")
|
|
247
|
+
: token.slice(1, -1).replaceAll("''", "'");
|
|
248
|
+
}
|
|
249
|
+
text += part + texts[index + 1];
|
|
250
|
+
});
|
|
251
|
+
return text;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (parts: DateParts) => {
|
|
256
|
+
const zone = parts.offset !== null ? "z" : "";
|
|
257
|
+
const shape = custom ? `custom${zone}` : `${parts.hasDate ? "d" : ""}${parts.hasTime ? "t" : ""}${zone}`;
|
|
258
|
+
const item = intl(shape, () => {
|
|
259
|
+
const defaults: Intl.DateTimeFormatOptions = custom ? options : {};
|
|
260
|
+
if (!custom && parts.hasDate) Object.assign(defaults, { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
261
|
+
if (!custom && parts.hasTime) Object.assign(defaults, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
262
|
+
// Values without an offset are wall-clock times: show them as they are.
|
|
263
|
+
return new Intl.DateTimeFormat(locale, { ...defaults, timeZone: parts.offset !== null ? timeZone : "UTC" });
|
|
264
|
+
});
|
|
265
|
+
const time = wallClock(parts).getTime() - (parts.offset ?? 0) * 60_000;
|
|
266
|
+
return item.format(time);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Whether a column holds numbers. */
|
|
271
|
+
export function isNumeric(column: Column) {
|
|
272
|
+
return column.type === "integer" || column.type === "bigint" || column.type === "decimal" || column.type === "float";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const TEMPORAL = new Set(["date", "time", "timestamp", "timestamptz"]);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The text of values of a column by its format, or null where the format says nothing about a value (the grid then
|
|
279
|
+
* shows its text form, §2.4). Formats that Intl refuses (an unknown currency or time zone) give null for every value.
|
|
280
|
+
*/
|
|
281
|
+
export function formatterFor(
|
|
282
|
+
column: Column,
|
|
283
|
+
format: ColumnFormat | undefined,
|
|
284
|
+
locale: string | undefined,
|
|
285
|
+
): ((value: JsonValue) => string | null) | null {
|
|
286
|
+
if (!format?.number && !format?.date) return null;
|
|
287
|
+
let number: ((value: number | string) => string) | null = null;
|
|
288
|
+
let date: ((parts: DateParts) => string) | null = null;
|
|
289
|
+
try {
|
|
290
|
+
if (format.number && isNumeric(column)) number = numberFormatter(locale, format.number, true);
|
|
291
|
+
if (format.date && TEMPORAL.has(column.type)) date = dateFormatter(locale, format.date);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof RangeError) return null;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
if (!number && !date) return null;
|
|
297
|
+
return (value) => {
|
|
298
|
+
if (value === null) return null;
|
|
299
|
+
if (number) {
|
|
300
|
+
const input = numberInput(value);
|
|
301
|
+
return input === null ? null : number(input);
|
|
302
|
+
}
|
|
303
|
+
if (date && typeof value === "string") {
|
|
304
|
+
const parts = parseDate(value.replace(/\+00$/, "Z"));
|
|
305
|
+
return parts ? date(parts) : null;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures over the rows of a view (§4.5 of the specification), for the column statistics and the totals row: both
|
|
3
|
+
* are an `aggregate` without keys over the plan's rows (Appendix A), read with `rows` like any view.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
type Capabilities,
|
|
7
|
+
type Column,
|
|
8
|
+
checkPlan,
|
|
9
|
+
type DisplayedColumn,
|
|
10
|
+
type Measure,
|
|
11
|
+
type MeasureName,
|
|
12
|
+
measureTakes,
|
|
13
|
+
type Plan,
|
|
14
|
+
planOptions,
|
|
15
|
+
} from "vastgrid-spec";
|
|
16
|
+
import type { TableInfo } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/** The rows of a plan before grouping: its derived columns and `where`. Null when the plan groups rows. */
|
|
19
|
+
function rowsPlan(plan: Plan): Plan | null {
|
|
20
|
+
if (plan.aggregate) return null;
|
|
21
|
+
return { ...(plan.derive ? { derive: plan.derive } : {}), ...(plan.where ? { where: plan.where } : {}) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether the engine evaluates a measure (§4.10: absent `measures` are all of them). */
|
|
25
|
+
export function offers(capabilities: Capabilities | undefined, fn: MeasureName): boolean {
|
|
26
|
+
if (capabilities?.plan?.aggregate === false) return false;
|
|
27
|
+
return capabilities?.measures?.includes(fn) ?? true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A statistic of a column: what it is, and the measure that gives it. */
|
|
31
|
+
export interface Statistic {
|
|
32
|
+
kind: "rows" | "values" | "distinct" | "min" | "max" | "sum" | "average" | "median" | "stddev";
|
|
33
|
+
measure: Measure;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const STATISTICS: [Statistic["kind"], MeasureName, number?][] = [
|
|
37
|
+
["values", "count"],
|
|
38
|
+
["distinct", "count_distinct"],
|
|
39
|
+
["min", "min"],
|
|
40
|
+
["max", "max"],
|
|
41
|
+
["sum", "sum"],
|
|
42
|
+
["average", "avg"],
|
|
43
|
+
["median", "quantile", 0.5],
|
|
44
|
+
["stddev", "stddev"],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** The statistics of a column that the engine evaluates and that take its type, with the number of rows first. */
|
|
48
|
+
export function statisticsOf(column: Column, capabilities: Capabilities | undefined): Statistic[] {
|
|
49
|
+
if (!offers(capabilities, "count")) return [];
|
|
50
|
+
const list: Statistic[] = [{ kind: "rows", measure: { name: "rows", fn: "count" } }];
|
|
51
|
+
for (const [kind, fn, p] of STATISTICS) {
|
|
52
|
+
if (!offers(capabilities, fn) || !measureTakes(fn, column)) continue;
|
|
53
|
+
list.push({ kind, measure: { name: kind, fn, column: column.name, ...(p === undefined ? {} : { p }) } });
|
|
54
|
+
}
|
|
55
|
+
return list;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The measures of the totals row (§5.1): one per displayed column with a total, named after the column. */
|
|
59
|
+
export function totalMeasures(displayed: readonly DisplayedColumn[]): Measure[] {
|
|
60
|
+
return displayed.flatMap(({ column, setting }) =>
|
|
61
|
+
setting.total
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
name: column.name,
|
|
65
|
+
fn: setting.total.fn,
|
|
66
|
+
column: column.name,
|
|
67
|
+
...(setting.total.p === undefined ? {} : { p: setting.total.p }),
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: [],
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The plan of measures over the rows of a plan, or null when it groups rows. */
|
|
75
|
+
export function measuresPlan(plan: Plan, measures: Measure[]): Plan | null {
|
|
76
|
+
const rows = rowsPlan(plan);
|
|
77
|
+
return rows && { ...rows, aggregate: { keys: [], measures } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The measures that a total can be, for a column's type, in the order the menu offers them. */
|
|
81
|
+
export const TOTALS: MeasureName[] = ["sum", "avg", "count", "count_distinct", "min", "max"];
|
|
82
|
+
|
|
83
|
+
/** A measure's name as 0.1 wrote it, `sum(amount)` or `count(*)`, with ` (2)`, ` (3)`, … when it is taken. */
|
|
84
|
+
export function measureName(fn: MeasureName, column: string | undefined, taken: ReadonlySet<string>): string {
|
|
85
|
+
const base = `${fn}(${column ?? "*"})`;
|
|
86
|
+
let name = base;
|
|
87
|
+
for (let n = 2; taken.has(name); n++) name = `${base} (${n})`;
|
|
88
|
+
return name;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The columns of the rows before grouping: the table's and the derived ones, or the table's when the plan fails. */
|
|
92
|
+
export function rowColumns(plan: Plan | undefined, table: TableInfo): readonly Column[] {
|
|
93
|
+
try {
|
|
94
|
+
return checkPlan(rowsPlan({ ...plan, aggregate: undefined }) ?? {}, table.columns, planOptions(table.capabilities))
|
|
95
|
+
.rows;
|
|
96
|
+
} catch {
|
|
97
|
+
return table.columns;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ids of requests (§8.1 of the specification) for several clients of one engine, such as the Apps and the grids of
|
|
3
|
+
* an MCP server or the webviews of an editor. Each client chooses ids of its own, which the engine would mix up (a `cancel` of one
|
|
4
|
+
* client's request would stop another's): the requests of each client get ids that are unique in the engine, and
|
|
5
|
+
* the answers get the client's ids back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type Json = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
/** What the clients share: the engine's `handle`, as `DuckDBEngine` has it. */
|
|
11
|
+
export interface Handler {
|
|
12
|
+
handle(message: unknown, options?: { signal?: AbortSignal }): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isObject(value: unknown): value is Json {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isId(value: unknown): value is number {
|
|
20
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class RequestIds {
|
|
24
|
+
private next = 1;
|
|
25
|
+
|
|
26
|
+
constructor(private readonly engine: Handler) {}
|
|
27
|
+
|
|
28
|
+
/** A client of the engine whose requests keep their ids apart from those of the others. */
|
|
29
|
+
client(): Handler {
|
|
30
|
+
// The engine's ids of this client's requests in flight, by the client's ids.
|
|
31
|
+
const running = new Map<number, number>();
|
|
32
|
+
return {
|
|
33
|
+
handle: async (message, options) => {
|
|
34
|
+
// A message without a valid id is the engine's to refuse, as it is (§8.1).
|
|
35
|
+
if (!isObject(message) || !isId(message.id)) return this.engine.handle(message, options);
|
|
36
|
+
const own = message.id;
|
|
37
|
+
const id = this.next++;
|
|
38
|
+
const sent: Json = { ...message, id };
|
|
39
|
+
if (message.op === "cancel" && isId(message.request)) {
|
|
40
|
+
// A request that is not running any more is cancelled under an id that no request has.
|
|
41
|
+
sent.request = running.get(message.request) ?? this.next++;
|
|
42
|
+
} else {
|
|
43
|
+
running.set(own, id);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const answer = await this.engine.handle(sent, options);
|
|
47
|
+
return isObject(answer) && answer.id === id ? { ...answer, id: own } : answer;
|
|
48
|
+
} finally {
|
|
49
|
+
if (running.get(own) === id) running.delete(own);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/core/rows.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { type CheckedPlan, type Column, canonicalJson, decodeBlock, type Plan } from "vastgrid-spec";
|
|
2
|
+
import { BLOCK_ROWS, type Read, type ViewCache } from "./cache.js";
|
|
3
|
+
import type { GridClient } from "./client.js";
|
|
4
|
+
import type { RowsResult, TableInfo, ViewInfo } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** Where the canvas is: its first row, how many rows it shows and the columns it draws. */
|
|
7
|
+
export interface Viewport {
|
|
8
|
+
top: number;
|
|
9
|
+
rows: number;
|
|
10
|
+
columns: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A view of the document's plan, being read or on screen. */
|
|
14
|
+
export interface View {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
readonly plan: Plan;
|
|
17
|
+
readonly checked: CheckedPlan;
|
|
18
|
+
/** The table's revision and edits version that the view's rows belong to. */
|
|
19
|
+
readonly revision: string | null;
|
|
20
|
+
readonly edits: number;
|
|
21
|
+
info: ViewInfo | null;
|
|
22
|
+
readonly cache: ViewCache;
|
|
23
|
+
readonly abort: AbortController;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether two lists of columns have the same names and types. */
|
|
27
|
+
export function sameColumns(a: readonly Column[], b: readonly Column[]) {
|
|
28
|
+
return a.length === b.length && a.every((column, index) => canonicalJson(column) === canonicalJson(b[index]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a `RowReader` reads with: the views, the connection and the state of its store, and where rows go. */
|
|
32
|
+
export interface ReadHost {
|
|
33
|
+
/** The view on screen. */
|
|
34
|
+
current(): View | null;
|
|
35
|
+
/** The view being made, which comes on screen once its first rows are read (§4.2 of react.md). */
|
|
36
|
+
next(): View | null;
|
|
37
|
+
client(): GridClient | null;
|
|
38
|
+
table(): TableInfo | null;
|
|
39
|
+
/** Whether the table is open for the current connection. */
|
|
40
|
+
opened(): boolean;
|
|
41
|
+
/** The document's row key column (§5.3). */
|
|
42
|
+
rowKey(): string | undefined;
|
|
43
|
+
/** Whether the session's active cell has the key of its row (§5.2). */
|
|
44
|
+
keyed(): boolean;
|
|
45
|
+
/** A read changed the info of the view on screen. */
|
|
46
|
+
showView(info: ViewInfo): void;
|
|
47
|
+
/** Rows of the view on screen arrived. */
|
|
48
|
+
showRows(): void;
|
|
49
|
+
/** A read of the view on screen or of the one being made failed. */
|
|
50
|
+
fail(error: unknown): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads the rows of a store's views into their caches (§4.1 of react.md): those on screen and a page around
|
|
55
|
+
* them, within a budget of bytes, and those that the store asks for. Rows that come for a view that is no longer
|
|
56
|
+
* on screen or being made are dropped.
|
|
57
|
+
*/
|
|
58
|
+
export class RowReader {
|
|
59
|
+
/** What the canvas shows, as `setViewport` was told last. */
|
|
60
|
+
viewport: Viewport = { top: 0, rows: 0, columns: [] };
|
|
61
|
+
private scheduled = false;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly host: ReadHost,
|
|
65
|
+
private readonly budget: number,
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
/** Follows what the canvas shows, and reads those rows and a page around them. */
|
|
69
|
+
setViewport(viewport: Viewport) {
|
|
70
|
+
const before = this.viewport;
|
|
71
|
+
if (
|
|
72
|
+
before.top === viewport.top &&
|
|
73
|
+
before.rows === viewport.rows &&
|
|
74
|
+
before.columns.length === viewport.columns.length &&
|
|
75
|
+
before.columns.every((column, index) => column === viewport.columns[index])
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.viewport = viewport;
|
|
80
|
+
this.schedule();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Reads rows `[start, end)` of `columns` of the view on screen if they are not read, for cells React renders. */
|
|
84
|
+
ensure(start: number, end: number, columns: readonly string[]) {
|
|
85
|
+
const view = this.host.current();
|
|
86
|
+
if (!view?.info || !this.host.opened() || this.host.next() || view.abort.signal.aborted) return;
|
|
87
|
+
for (const read of view.cache.reads(start, end, this.withKey(columns), view.info.rowCount)) {
|
|
88
|
+
void this.read(view, read).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reads the rows on screen once the current task is done, once for all the changes it makes. */
|
|
93
|
+
schedule() {
|
|
94
|
+
if (this.scheduled) return;
|
|
95
|
+
this.scheduled = true;
|
|
96
|
+
queueMicrotask(() => {
|
|
97
|
+
this.scheduled = false;
|
|
98
|
+
this.readVisible();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The rows from `row` for a viewport, in whole blocks. */
|
|
103
|
+
around(row: number, rowCount: number | null, columns: readonly string[]): Read {
|
|
104
|
+
const start = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
|
|
105
|
+
const rows = Math.max(this.viewport.rows, 1);
|
|
106
|
+
let end = Math.ceil((row + rows) / BLOCK_ROWS) * BLOCK_ROWS;
|
|
107
|
+
if (rowCount !== null) end = Math.min(end, Math.max(rowCount, start + 1));
|
|
108
|
+
return { start, end: Math.max(end, start + 1), columns: [...columns] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The columns of a view to read: those asked for that it has and its row key column, at least one. */
|
|
112
|
+
withKeyFor(view: View, columns: readonly string[]): string[] {
|
|
113
|
+
const names = new Set(view.checked.columns.map((column) => column.name));
|
|
114
|
+
const wanted = columns.filter((column) => names.has(column));
|
|
115
|
+
const key = this.host.rowKey();
|
|
116
|
+
if (key !== undefined && names.has(key) && !wanted.includes(key)) wanted.push(key);
|
|
117
|
+
if (wanted.length === 0 && view.checked.columns.length > 0) wanted.push(view.checked.columns[0].name);
|
|
118
|
+
return wanted;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Reads the rows that a command needs (`not_loaded`, §6.1), in whole blocks. */
|
|
122
|
+
async load(view: View, details: { rows: [number, number][]; columns: string[] }) {
|
|
123
|
+
const rowCount = view.info?.rowCount ?? 0;
|
|
124
|
+
const columns = this.withKeyFor(view, details.columns);
|
|
125
|
+
const reads: Promise<void>[] = [];
|
|
126
|
+
for (const [start, end] of details.rows) {
|
|
127
|
+
const first = Math.floor(start / BLOCK_ROWS) * BLOCK_ROWS;
|
|
128
|
+
const last = Math.min(rowCount, Math.ceil(end / BLOCK_ROWS) * BLOCK_ROWS);
|
|
129
|
+
for (let at = first; at < last; at += BLOCK_ROWS * 16) {
|
|
130
|
+
reads.push(this.read(view, { start: at, end: Math.min(last, at + BLOCK_ROWS * 16), columns }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await Promise.all(reads);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Reads rows of a view into its cache. */
|
|
137
|
+
async read(view: View, read: Read): Promise<void> {
|
|
138
|
+
const client = this.host.client();
|
|
139
|
+
const table = this.host.table();
|
|
140
|
+
if (!client || !table) return;
|
|
141
|
+
view.cache.reading(read, true);
|
|
142
|
+
try {
|
|
143
|
+
const result = await client.request<RowsResult>(
|
|
144
|
+
"rows",
|
|
145
|
+
{
|
|
146
|
+
table: table.table,
|
|
147
|
+
revision: view.revision,
|
|
148
|
+
plan: view.plan,
|
|
149
|
+
start: read.start,
|
|
150
|
+
end: read.end,
|
|
151
|
+
columns: read.columns,
|
|
152
|
+
// A keyed selection needs locate on the complete view before its position can change.
|
|
153
|
+
...(!view.info && table.capabilities.preview && !this.host.keyed() ? { preview: true } : {}),
|
|
154
|
+
},
|
|
155
|
+
view.abort.signal,
|
|
156
|
+
);
|
|
157
|
+
if (view !== this.host.current() && view !== this.host.next()) return;
|
|
158
|
+
const block = decodeBlock(result.block.bytes);
|
|
159
|
+
view.cache.receive(
|
|
160
|
+
result.start,
|
|
161
|
+
result.rowIds,
|
|
162
|
+
block.columns.map((column) => column.name),
|
|
163
|
+
block.rows,
|
|
164
|
+
result.truncated,
|
|
165
|
+
);
|
|
166
|
+
const info = result.view;
|
|
167
|
+
view.cache.reading(read, false);
|
|
168
|
+
// A ready event can arrive before the response containing the preview.
|
|
169
|
+
if (
|
|
170
|
+
!(view.info?.status === "ready" && info.status === "building") &&
|
|
171
|
+
(!view.info ||
|
|
172
|
+
view.info.rowCount !== info.rowCount ||
|
|
173
|
+
view.info.status !== info.status ||
|
|
174
|
+
view.info.readableRows !== info.readableRows)
|
|
175
|
+
) {
|
|
176
|
+
view.info =
|
|
177
|
+
view.info && sameColumns(view.info.columns, info.columns) ? { ...info, columns: view.info.columns } : info;
|
|
178
|
+
if (view === this.host.current()) this.host.showView(view.info);
|
|
179
|
+
}
|
|
180
|
+
if (view === this.host.current()) this.host.showRows();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
view.cache.reading(read, false);
|
|
183
|
+
if (view === this.host.current() || view === this.host.next()) this.host.fail(error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The columns to read: those asked for, and the row key column (§5.3), which commands need. */
|
|
189
|
+
private withKey(columns: readonly string[]): string[] {
|
|
190
|
+
const key = this.host.rowKey();
|
|
191
|
+
const view = this.host.current()?.info?.columns ?? [];
|
|
192
|
+
if (key === undefined || columns.includes(key) || !view.some((column) => column.name === key)) {
|
|
193
|
+
return [...columns];
|
|
194
|
+
}
|
|
195
|
+
return [...columns, key];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private readVisible() {
|
|
199
|
+
const view = this.host.current();
|
|
200
|
+
if (!view?.info || !this.host.opened() || this.host.next() || view.abort.signal.aborted) return;
|
|
201
|
+
const { top, rows, columns } = this.viewport;
|
|
202
|
+
if (columns.length === 0) return;
|
|
203
|
+
const margin = Math.max(rows, 32);
|
|
204
|
+
const start = Math.max(0, top - margin);
|
|
205
|
+
const readable = view.info.status === "building" ? view.info.readableRows : view.info.rowCount;
|
|
206
|
+
const end = Math.min(readable, top + rows + margin);
|
|
207
|
+
view.cache.touch(top, top + rows);
|
|
208
|
+
for (const read of view.cache.reads(start, end, this.withKey(columns), readable)) {
|
|
209
|
+
void this.read(view, read).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
view.cache.evict(this.budget, start, end);
|
|
212
|
+
}
|
|
213
|
+
}
|