@dou-ui/pivot 0.0.0-stage → 0.1.0-alpha.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/assets/default-PXde2wzm.js.map +1 -0
  4. package/dist/core/format.js +116 -0
  5. package/dist/core/format.js.map +1 -0
  6. package/dist/core/labels.js +28 -0
  7. package/dist/core/labels.js.map +1 -0
  8. package/dist/core/layout.js +190 -0
  9. package/dist/core/layout.js.map +1 -0
  10. package/dist/core/source.js +46 -0
  11. package/dist/core/source.js.map +1 -0
  12. package/dist/core/table.js +154 -0
  13. package/dist/core/table.js.map +1 -0
  14. package/dist/engine/aggregate.js +51 -0
  15. package/dist/engine/aggregate.js.map +1 -0
  16. package/dist/engine/compute.js +735 -0
  17. package/dist/engine/compute.js.map +1 -0
  18. package/dist/engine/engine.js +46 -0
  19. package/dist/engine/engine.js.map +1 -0
  20. package/dist/engine/formula.js +225 -0
  21. package/dist/engine/formula.js.map +1 -0
  22. package/dist/engine/group.js +107 -0
  23. package/dist/engine/group.js.map +1 -0
  24. package/dist/engine/hash.js +108 -0
  25. package/dist/engine/hash.js.map +1 -0
  26. package/dist/engine/index.js +8 -0
  27. package/dist/engine/show.js +132 -0
  28. package/dist/engine/show.js.map +1 -0
  29. package/dist/engine/source.js +339 -0
  30. package/dist/engine/source.js.map +1 -0
  31. package/dist/engine/tree.js +226 -0
  32. package/dist/engine/tree.js.map +1 -0
  33. package/dist/fields/fields.js +6 -0
  34. package/dist/fields/fields.js.map +1 -0
  35. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js +881 -0
  36. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/fields/filter-list.js +6 -0
  38. package/dist/fields/filter-list.js.map +1 -0
  39. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js +184 -0
  40. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/fields/formula-dialog.js +6 -0
  42. package/dist/fields/formula-dialog.js.map +1 -0
  43. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js +165 -0
  44. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/fields/model.js +200 -0
  46. package/dist/fields/model.js.map +1 -0
  47. package/dist/fields/top-dialog.js +6 -0
  48. package/dist/fields/top-dialog.js.map +1 -0
  49. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js +174 -0
  50. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/index.js +16 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/locale.js +508 -0
  54. package/dist/locale.js.map +1 -0
  55. package/dist/pivot.js +6 -0
  56. package/dist/pivot.js.map +1 -0
  57. package/dist/pivot.vue_vue_type_script_setup_true_lang.js +1711 -0
  58. package/dist/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/style.css +1 -0
  60. package/dist/style.d.ts +1 -0
  61. package/dist/style.js +13 -0
  62. package/dist/types/core/format.d.ts +26 -0
  63. package/dist/types/core/labels.d.ts +14 -0
  64. package/dist/types/core/layout.d.ts +72 -0
  65. package/dist/types/core/source.d.ts +7 -0
  66. package/dist/types/core/table.d.ts +73 -0
  67. package/dist/types/engine/aggregate.d.ts +35 -0
  68. package/dist/types/engine/compute.d.ts +173 -0
  69. package/dist/types/engine/engine.d.ts +29 -0
  70. package/dist/types/engine/formula.d.ts +44 -0
  71. package/dist/types/engine/group.d.ts +34 -0
  72. package/dist/types/engine/hash.d.ts +36 -0
  73. package/dist/types/engine/index.d.ts +15 -0
  74. package/dist/types/engine/show.d.ts +27 -0
  75. package/dist/types/engine/source.d.ts +108 -0
  76. package/dist/types/engine/tree.d.ts +87 -0
  77. package/dist/types/engine/types.d.ts +148 -0
  78. package/dist/types/fields/fields.vue.d.ts +26 -0
  79. package/dist/types/fields/filter-list.vue.d.ts +20 -0
  80. package/dist/types/fields/formula-dialog.vue.d.ts +30 -0
  81. package/dist/types/fields/model.d.ts +52 -0
  82. package/dist/types/fields/top-dialog.vue.d.ts +25 -0
  83. package/dist/types/index.d.ts +458 -0
  84. package/dist/types/locale.d.ts +184 -0
  85. package/dist/types/pivot.vue.d.ts +29 -0
  86. package/dist/types/types.d.ts +199 -0
  87. package/dist/types/worker/client.d.ts +19 -0
  88. package/dist/types/worker/default.d.ts +1 -0
  89. package/dist/types/worker/index.d.ts +5 -0
  90. package/dist/types/worker/protocol.d.ts +60 -0
  91. package/dist/types/worker/serve.d.ts +22 -0
  92. package/dist/worker/client.js +191 -0
  93. package/dist/worker/client.js.map +1 -0
  94. package/dist/worker/default.js +20 -0
  95. package/dist/worker/default.js.map +1 -0
  96. package/dist/worker/index.js +3 -0
  97. package/dist/worker/serve.js +101 -0
  98. package/dist/worker/serve.js.map +1 -0
  99. package/package.json +64 -4
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes } from 'vue';
2
+ import type { ForwardedAttributes } from 'dou-ui';
3
+ import type { PivotCollapsed, PivotConfig } from './engine/types';
4
+ import type { PivotCellEvent, PivotExpose, PivotProps } from './types';
5
+ declare const __VLS_export: <Row = Record<string, unknown>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
6
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<(PivotProps<Row> & ForwardedAttributes<HTMLAttributes, PivotProps<Row>, "update:config" | "update:collapsed" | "cellClick"> & {
7
+ config?: PivotConfig;
8
+ collapsed?: PivotCollapsed;
9
+ }) & {
10
+ "onUpdate:config"?: ((value: PivotConfig) => any) | undefined;
11
+ "onUpdate:collapsed"?: ((value: PivotCollapsed) => any) | undefined;
12
+ onCellClick?: ((cell: PivotCellEvent<Row>) => any) | undefined;
13
+ }> & (typeof globalThis extends {
14
+ __VLS_PROPS_FALLBACK: infer P;
15
+ } ? P : {});
16
+ expose: (exposed: import("vue").ShallowUnwrapRef<PivotExpose>) => void;
17
+ attrs: any;
18
+ slots: {};
19
+ emit: ((evt: "cellClick", cell: PivotCellEvent<Row>) => void) & (((event: "update:config", value: PivotConfig) => void) & ((event: "update:collapsed", value: PivotCollapsed) => void));
20
+ }>) => import("vue").VNode & {
21
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
22
+ };
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
25
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
26
+ [K in keyof T]: T[K];
27
+ } : {
28
+ [K in keyof T as K]: T[K];
29
+ }) & {};
@@ -0,0 +1,199 @@
1
+ import type { Size } from 'dou-ui';
2
+ import type { PivotAggregator } from './engine/aggregate';
3
+ import type { PivotResult } from './engine/compute';
4
+ import type { PivotEngine } from './engine/engine';
5
+ import type { PivotData } from './engine/source';
6
+ import type { PivotAxis } from './core/layout';
7
+ import type { PivotCSVOptions, PivotGrid } from './core/table';
8
+ import type { PivotCollapsed, PivotConfig, PivotField, PivotPath } from './engine/types';
9
+ import type { PivotMessagesOverride } from './locale';
10
+ export interface PivotProps<Row = Record<string, unknown>> {
11
+ /**
12
+ * The records: an array of objects, or their columns
13
+ * (`{ region: [...], amount: [...] }`, typed arrays welcome). Replace it
14
+ * to update it (changes inside are not seen); keep large data in a
15
+ * `shallowRef`.
16
+ */
17
+ data: PivotData<Row>;
18
+ /** What the fields hold and how to show them; worked out from the records when left out. */
19
+ fields?: readonly PivotField<Row>[];
20
+ /** Which fields go where. */
21
+ config?: PivotConfig;
22
+ /** The collapsed groups; all groups are open by default. */
23
+ collapsed?: PivotCollapsed;
24
+ /**
25
+ * The most height, a number in px or any CSS length; a shorter table ends
26
+ * with its last row. The width fills the container.
27
+ */
28
+ maxHeight?: number | string;
29
+ /** Row height: 28, 32 (`md`, the default) or 40 px. */
30
+ size?: Size;
31
+ /** The width of the row headers, in px. */
32
+ rowHeaderWidth?: number;
33
+ /** The width of each value column, in px. */
34
+ columnWidth?: number;
35
+ /**
36
+ * `compact` (the default) puts the row fields in one column as a tree, a
37
+ * group's total on its own line; `tabular` gives each row field a column
38
+ * and an open group's total a line after its children.
39
+ */
40
+ layout?: 'compact' | 'tabular';
41
+ /** Whether column widths can be dragged (and fitted with a double-click); `true` by default. */
42
+ resizable?: boolean;
43
+ /** Ways to sum up beside the built-in ones, by the name the config uses. */
44
+ aggregators?: readonly PivotAggregator[];
45
+ /**
46
+ * Sum up in a worker, keeping the page responsive. `'auto'` (the default)
47
+ * does so above 100,000 records where workers are available.
48
+ */
49
+ worker?: boolean | 'auto';
50
+ /** Where to sum up instead: your own worker (`createPivotWorker`) or a server. */
51
+ engine?: PivotEngine;
52
+ /**
53
+ * Marks a row group and a column group, by their paths (as `cellClick`
54
+ * gives them), such as the groups a click picked; the cells where both
55
+ * meet stand out the most.
56
+ */
57
+ highlight?: {
58
+ row?: PivotPath;
59
+ column?: PivotPath;
60
+ };
61
+ /** The table's accessible name. */
62
+ ariaLabel?: string;
63
+ /** Texts to override. */
64
+ messages?: PivotMessagesOverride;
65
+ }
66
+ export interface PivotFieldsProps<Row = Record<string, unknown>> {
67
+ /** The same records as the table's; the panel reads them to list the fields and their values. */
68
+ data: PivotData<Row>;
69
+ /** The same fields as the table's. */
70
+ fields?: readonly PivotField<Row>[];
71
+ /** Which fields go where; bind the same config as the table's with `v-model:config`. */
72
+ config?: PivotConfig;
73
+ /** Ways to sum up beside the built-in ones, offered in each value's menu. */
74
+ aggregators?: readonly PivotAggregator[];
75
+ /**
76
+ * Show the panel as a button that opens it in a drawer. `'auto'` (the
77
+ * default) does so on screens narrower than the `sm` breakpoint.
78
+ */
79
+ drawer?: boolean | 'auto';
80
+ /** Texts to override. */
81
+ messages?: PivotMessagesOverride;
82
+ }
83
+ /** A cell that was clicked (or activated with Enter). */
84
+ export interface PivotCellEvent<Row = Record<string, unknown>> {
85
+ /** The row group's labels, `[]` for the grand total. */
86
+ row: PivotPath;
87
+ /** The column group's labels, `[]` for the grand total. */
88
+ column: PivotPath;
89
+ /** The same groups as the table writes them (`2026年1月` for the group `'2026-01'`). */
90
+ labels: {
91
+ row: string[];
92
+ column: string[];
93
+ };
94
+ /** The value's place in `config.values`. */
95
+ valueIndex: number;
96
+ /** The number in the cell, as shown (a share, when the value has `show`); `NaN` when it is empty. */
97
+ value: number;
98
+ /** The records summed up in the cell. */
99
+ records: () => Row[];
100
+ /** Their indexes in `data`. */
101
+ indices: () => number[];
102
+ }
103
+ export interface PivotEmits<Row = Record<string, unknown>> {
104
+ 'update:config': [config: PivotConfig];
105
+ 'update:collapsed': [collapsed: PivotCollapsed];
106
+ cellClick: [cell: PivotCellEvent<Row>];
107
+ }
108
+ export interface PivotExpose {
109
+ /** The groups and values on show. */
110
+ readonly result: PivotResult;
111
+ /** Opens every group, on one axis (`rows` or `columns`) or both. */
112
+ expandAll: (options?: {
113
+ axis?: PivotAxis;
114
+ }) => void;
115
+ /**
116
+ * Closes every group from `depth` in (1, the default, closes the outermost
117
+ * groups), on one axis or both.
118
+ */
119
+ collapseAll: (options?: {
120
+ axis?: PivotAxis;
121
+ depth?: number;
122
+ }) => void;
123
+ /** Opens or closes a group. */
124
+ toggle: (axis: PivotAxis, path: PivotPath, open?: boolean) => void;
125
+ /**
126
+ * The table as it shows, headers and all, as cells: each row field in a
127
+ * column of its own, the numbers as shown (shares, ranks…) with their text.
128
+ */
129
+ toGrid: () => PivotGrid;
130
+ /** The table as CSV, the numbers as values a spreadsheet can add up unless `text`. */
131
+ toCSV: (options?: PivotCSVOptions) => string;
132
+ /**
133
+ * The table as a workbook for `DouSheet`: numbers with their formats, the
134
+ * headers merged and frozen, the columns as wide as on screen.
135
+ */
136
+ toSheet: (options?: {
137
+ name?: string;
138
+ }) => PivotSheetWorkbook;
139
+ /** Rows and series for `@dou-ui/chart`, from the groups on show; totals are left out. */
140
+ toChartRows: (options?: PivotChartRowsOptions) => PivotChartRows;
141
+ /** Copies the selected cells (or, with `all`, the whole table) to the clipboard. */
142
+ copy: (options?: PivotCopyOptions) => Promise<void>;
143
+ /** Selects every cell, as Ctrl / ⌘ + A does. */
144
+ selectAll: () => void;
145
+ /** Prints the whole table on A4 pages, the column headers repeated on each. */
146
+ print: (options?: PivotPrintOptions) => void;
147
+ }
148
+ export interface PivotCopyOptions {
149
+ /** The whole table, headers and all, instead of the selected cells. */
150
+ all?: boolean;
151
+ }
152
+ export interface PivotPrintOptions {
153
+ /** A heading above the table; the table's `aria-label` by default. */
154
+ title?: string;
155
+ /** Landscape when the table is wider than a portrait page, by default. */
156
+ orientation?: 'portrait' | 'landscape';
157
+ }
158
+ export interface PivotChartRowsOptions {
159
+ /** Only this value (its place in `config.values`); every value on show by default. */
160
+ valueIndex?: number;
161
+ }
162
+ /** What a chart needs: `rows` as its `data`, `x` as its `x`, a series per entry of `series`. */
163
+ export interface PivotChartRows {
164
+ /** The key of each row's group label. */
165
+ x: 'label';
166
+ rows: Record<string, string | number | null>[];
167
+ series: {
168
+ /** The key of the series' numbers in each row. */
169
+ key: string;
170
+ /** The column's groups (and the value, when there are several). */
171
+ name: string;
172
+ valueIndex: number;
173
+ /** Writes a number as the table does, e.g. for the axis's `format`. */
174
+ format: (n: number) => string;
175
+ }[];
176
+ }
177
+ /** A cell as `DouSheet` takes it. */
178
+ export type PivotSheetCell = string | number | null | {
179
+ value: string | number;
180
+ format?: string;
181
+ style?: string;
182
+ };
183
+ /** A workbook in the shape `DouSheet`'s `v-model` takes. */
184
+ export interface PivotSheetWorkbook {
185
+ version: 1;
186
+ sheets: {
187
+ name: string;
188
+ cells: PivotSheetCell[][];
189
+ merges: string[];
190
+ freeze: {
191
+ rows: number;
192
+ cols: number;
193
+ };
194
+ colSizes: Record<number, number>;
195
+ }[];
196
+ styles: Record<string, {
197
+ bold?: boolean;
198
+ }>;
199
+ }
@@ -0,0 +1,19 @@
1
+ import { type PivotEngine } from '../engine/engine';
2
+ /** The page's side of the messages: a `Worker`, or anything that talks like one. */
3
+ export interface PivotWorkerLike {
4
+ postMessage(message: unknown, transfer?: Transferable[]): void;
5
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
6
+ /** When the worker fails to start or throws: pending and later computes reject. */
7
+ addEventListener(type: 'error', listener: (event: Event) => void): void;
8
+ terminate?(): void;
9
+ }
10
+ /**
11
+ * An engine that sums up in a worker, so a million records never hold up
12
+ * the page. The page reads each field the config needs once, in slices of
13
+ * a few milliseconds, and moves it to the worker (no copy); after that
14
+ * only configs go over.
15
+ *
16
+ * Pass a worker that calls `servePivot`, or a function that makes one
17
+ * (made when first needed).
18
+ */
19
+ export declare function createPivotWorker(worker: PivotWorkerLike | (() => PivotWorkerLike)): PivotEngine;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export { createPivotWorker } from './client';
2
+ export type { PivotWorkerLike } from './client';
3
+ export { servePivot } from './serve';
4
+ export type { PivotWorkerScope, ServePivotOptions } from './serve';
5
+ export type { PivotEngine, PivotComputeRequest } from '../engine/engine';
@@ -0,0 +1,60 @@
1
+ import type { PivotPacket } from '../engine/compute';
2
+ import type { PivotEncodedColumn } from '../engine/source';
3
+ import type { PivotConfig, PivotFieldType } from '../engine/types';
4
+ /** A field as the worker knows it: no functions, they stay in the page. */
5
+ export interface PivotWorkerField {
6
+ name: string;
7
+ label: string;
8
+ type: PivotFieldType;
9
+ order?: readonly unknown[];
10
+ }
11
+ /** What the page sends. `source` numbers the records; anything for an older one is dropped. */
12
+ export type PivotToWorker = {
13
+ type: 'source';
14
+ source: number;
15
+ length: number;
16
+ locale?: string;
17
+ fields: PivotWorkerField[];
18
+ }
19
+ /** Fields the page came to know after `source` (keys the config names that no field described). */
20
+ | {
21
+ type: 'fields';
22
+ source: number;
23
+ fields: PivotWorkerField[];
24
+ } | ({
25
+ type: 'dimension';
26
+ source: number;
27
+ name: string;
28
+ } & PivotEncodedColumn) | {
29
+ type: 'measure';
30
+ source: number;
31
+ name: string;
32
+ values: Float64Array;
33
+ } | {
34
+ type: 'compute';
35
+ source: number;
36
+ id: number;
37
+ config: PivotConfig;
38
+ };
39
+ /** What the worker answers to a `compute`. */
40
+ export type PivotFromWorker = {
41
+ type: 'result';
42
+ id: number;
43
+ packet: PivotPacket;
44
+ }
45
+ /** Fields it was not sent, which the page could not tell it needs (a custom aggregator's input). */
46
+ | {
47
+ type: 'need';
48
+ id: number;
49
+ dimensions: string[];
50
+ measures: string[];
51
+ }
52
+ /** A newer compute came in first. */
53
+ | {
54
+ type: 'skipped';
55
+ id: number;
56
+ } | {
57
+ type: 'error';
58
+ id: number;
59
+ message: string;
60
+ };
@@ -0,0 +1,22 @@
1
+ import type { PivotAggregator } from '../engine/aggregate';
2
+ export interface ServePivotOptions {
3
+ /** Custom aggregators (`defineAggregator`) the configs may name; functions cannot be sent, so they live here. */
4
+ aggregators?: readonly PivotAggregator[];
5
+ }
6
+ /** The worker's side of the messages: anything with `postMessage` and `onmessage`. */
7
+ export interface PivotWorkerScope {
8
+ postMessage(message: unknown, transfer?: Transferable[]): void;
9
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
10
+ }
11
+ /**
12
+ * Sums up for the page in a worker. Call it in your own worker file when
13
+ * configs use custom aggregators; `DouPivot`'s built-in worker calls it
14
+ * with none.
15
+ *
16
+ * ```ts
17
+ * // pivot.worker.ts
18
+ * import { servePivot } from '@dou-ui/pivot/worker'
19
+ * servePivot({ aggregators: [weightedAverage] })
20
+ * ```
21
+ */
22
+ export declare function servePivot(options?: ServePivotOptions, scope?: PivotWorkerScope): void;
@@ -0,0 +1,191 @@
1
+ import { PivotResult } from "../engine/compute.js";
2
+ import { neededColumns } from "../engine/engine.js";
3
+ // src/worker/client.ts
4
+ /** How long the page reads records before it lets other work run, in ms. */
5
+ var SLICE = 6;
6
+ var PREFIX = "[@dou-ui/pivot]";
7
+ var CANNOT_START = "the worker could not start";
8
+ /** Why the worker can't sum up, with the package prefix in front of what the browser said. */
9
+ var brokenMessage = (message) => !message ? `${PREFIX} ${CANNOT_START}; check that the worker file loads (a 404, a CSP or cross-origin block, or a bundler that left it out), or use :worker="false" or createPivotWorker()` : message.startsWith(PREFIX) ? message : `${PREFIX} ${CANNOT_START}: ${message}`;
10
+ var abortError = () => new DOMException("A newer config came in", "AbortError");
11
+ /** Lets the page paint and handle input between slices of reading. */
12
+ var breathe = (() => {
13
+ if (typeof MessageChannel === "undefined") return () => new Promise((r) => setTimeout(r));
14
+ const channel = new MessageChannel();
15
+ const waiting = [];
16
+ channel.port1.addEventListener("message", () => waiting.shift()?.());
17
+ channel.port1.start();
18
+ return () => new Promise((resolve) => {
19
+ waiting.push(resolve);
20
+ channel.port2.postMessage(0);
21
+ });
22
+ })();
23
+ /** Runs reading in slices of a few ms each, so the page stays responsive. */
24
+ async function sliced(steps, signal) {
25
+ let until = performance.now() + SLICE;
26
+ for (;;) {
27
+ const step = steps.next();
28
+ if (step.done) return step.value;
29
+ if (performance.now() < until) continue;
30
+ await breathe();
31
+ if (signal?.aborted) throw abortError();
32
+ until = performance.now() + SLICE;
33
+ }
34
+ }
35
+ var describe = (source) => [...source.fields.values()].map((f) => ({
36
+ name: f.name,
37
+ label: f.label,
38
+ type: f.type,
39
+ ...f.order ? { order: f.order } : {}
40
+ }));
41
+ /**
42
+ * An engine that sums up in a worker, so a million records never hold up
43
+ * the page. The page reads each field the config needs once, in slices of
44
+ * a few milliseconds, and moves it to the worker (no copy); after that
45
+ * only configs go over.
46
+ *
47
+ * Pass a worker that calls `servePivot`, or a function that makes one
48
+ * (made when first needed).
49
+ */
50
+ function createPivotWorker(worker) {
51
+ let w = null;
52
+ let current = null;
53
+ let sourceId = 0;
54
+ let nextId = 0;
55
+ /** Why the worker stopped working, once it has. */
56
+ let broken = null;
57
+ const known = /* @__PURE__ */ new Set();
58
+ const sent = {
59
+ dimensions: /* @__PURE__ */ new Set(),
60
+ measures: /* @__PURE__ */ new Set()
61
+ };
62
+ const waiting = /* @__PURE__ */ new Map();
63
+ let lane = Promise.resolve();
64
+ const post = (message, transfer) => w.postMessage(message, transfer);
65
+ const connect = () => {
66
+ if (w) return w;
67
+ w = typeof worker === "function" ? worker() : worker;
68
+ w.addEventListener("message", (event) => {
69
+ const m = event.data;
70
+ const job = waiting.get(m.id);
71
+ if (!job) return;
72
+ waiting.delete(m.id);
73
+ job.resolve(m);
74
+ });
75
+ w.addEventListener("error", (event) => {
76
+ broken = brokenMessage(event.message);
77
+ for (const job of waiting.values()) job.resolve({
78
+ type: "error",
79
+ id: -1,
80
+ message: broken
81
+ });
82
+ waiting.clear();
83
+ });
84
+ return w;
85
+ };
86
+ /** Reads and sends the fields the worker does not have yet. */
87
+ async function ship(source, need, signal) {
88
+ const fresh = describe(source).filter((f) => !known.has(f.name));
89
+ if (fresh.length) {
90
+ for (const f of fresh) known.add(f.name);
91
+ post({
92
+ type: "fields",
93
+ source: sourceId,
94
+ fields: fresh
95
+ });
96
+ }
97
+ for (const name of need.dimensions) {
98
+ if (sent.dimensions.has(name)) continue;
99
+ const id = sourceId;
100
+ const column = await sliced(source.encodeSteps(name), signal);
101
+ if (id !== sourceId) throw abortError();
102
+ post({
103
+ type: "dimension",
104
+ source: id,
105
+ name,
106
+ ...column
107
+ }, [column.codes.buffer]);
108
+ sent.dimensions.add(name);
109
+ }
110
+ for (const name of need.measures) {
111
+ if (sent.measures.has(name)) continue;
112
+ const id = sourceId;
113
+ const values = await sliced(source.measureSteps(name, true), signal);
114
+ if (id !== sourceId) throw abortError();
115
+ post({
116
+ type: "measure",
117
+ source: id,
118
+ name,
119
+ values
120
+ }, [values.buffer]);
121
+ sent.measures.add(name);
122
+ }
123
+ }
124
+ return {
125
+ async compute(source, config, request = {}) {
126
+ const { signal } = request;
127
+ try {
128
+ connect();
129
+ } catch (error) {
130
+ broken = brokenMessage(error.message);
131
+ }
132
+ if (broken) throw new Error(broken);
133
+ if (source !== current) {
134
+ current = source;
135
+ sourceId++;
136
+ known.clear();
137
+ sent.dimensions.clear();
138
+ sent.measures.clear();
139
+ const fields = describe(source);
140
+ for (const f of fields) known.add(f.name);
141
+ post({
142
+ type: "source",
143
+ source: sourceId,
144
+ length: source.length,
145
+ locale: source.locale,
146
+ fields
147
+ });
148
+ }
149
+ const need = neededColumns(source, config, request.aggregators);
150
+ const shipped = lane.then(() => ship(source, need, signal));
151
+ lane = shipped.catch(() => {});
152
+ await shipped;
153
+ for (let round = 0; round < 3; round++) {
154
+ if (signal?.aborted) throw abortError();
155
+ const id = nextId++;
156
+ const answer = await new Promise((resolve) => {
157
+ if (broken) return resolve({
158
+ type: "error",
159
+ id,
160
+ message: broken
161
+ });
162
+ waiting.set(id, { resolve });
163
+ post({
164
+ type: "compute",
165
+ source: sourceId,
166
+ id,
167
+ config
168
+ });
169
+ });
170
+ if (answer.type === "result") return PivotResult.unpack(answer.packet, source, config);
171
+ if (answer.type === "error") throw new Error(answer.message);
172
+ if (answer.type === "skipped") throw abortError();
173
+ await ship(source, answer, signal);
174
+ }
175
+ throw new Error("[@dou-ui/pivot] the worker still lacks fields after they were sent");
176
+ },
177
+ dispose() {
178
+ w?.terminate?.();
179
+ w = null;
180
+ current = null;
181
+ for (const job of waiting.values()) job.resolve({
182
+ type: "skipped",
183
+ id: -1
184
+ });
185
+ waiting.clear();
186
+ }
187
+ };
188
+ }
189
+ export { createPivotWorker };
190
+
191
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/worker/client.ts"],"sourcesContent":["import { PivotResult } from '../engine/compute'\nimport { neededColumns, type PivotComputeRequest, type PivotEngine } from '../engine/engine'\nimport type { PivotSource } from '../engine/source'\nimport type { PivotConfig } from '../engine/types'\nimport type { PivotFromWorker, PivotToWorker, PivotWorkerField } from './protocol'\n\n/** The page's side of the messages: a `Worker`, or anything that talks like one. */\nexport interface PivotWorkerLike {\n postMessage(message: unknown, transfer?: Transferable[]): void\n addEventListener(type: 'message', listener: (event: MessageEvent) => void): void\n /** When the worker fails to start or throws: pending and later computes reject. */\n addEventListener(type: 'error', listener: (event: Event) => void): void\n terminate?(): void\n}\n\n/** How long the page reads records before it lets other work run, in ms. */\nconst SLICE = 6\n\nconst PREFIX = '[@dou-ui/pivot]'\nconst CANNOT_START = 'the worker could not start'\n/** Why the worker can't sum up, with the package prefix in front of what the browser said. */\nconst brokenMessage = (message: string | undefined) =>\n !message\n ? `${PREFIX} ${CANNOT_START}; check that the worker file loads (a 404, a CSP or cross-origin block, or a bundler that left it out), or use :worker=\"false\" or createPivotWorker()`\n : message.startsWith(PREFIX)\n ? message\n : `${PREFIX} ${CANNOT_START}: ${message}`\n\nconst abortError = () => new DOMException('A newer config came in', 'AbortError')\n\n/** Lets the page paint and handle input between slices of reading. */\nconst breathe = (() => {\n if (typeof MessageChannel === 'undefined') return () => new Promise((r) => setTimeout(r))\n const channel = new MessageChannel()\n const waiting: (() => void)[] = []\n channel.port1.addEventListener('message', () => waiting.shift()?.())\n channel.port1.start()\n return () =>\n new Promise<void>((resolve) => {\n waiting.push(resolve)\n channel.port2.postMessage(0)\n })\n})()\n\n/** Runs reading in slices of a few ms each, so the page stays responsive. */\nasync function sliced<T>(steps: Generator<void, T>, signal?: AbortSignal): Promise<T> {\n let until = performance.now() + SLICE\n for (;;) {\n const step = steps.next()\n if (step.done) return step.value\n if (performance.now() < until) continue\n await breathe()\n if (signal?.aborted) throw abortError()\n until = performance.now() + SLICE\n }\n}\n\nconst describe = <Row>(source: PivotSource<Row>): PivotWorkerField[] =>\n [...source.fields.values()].map((f) => ({\n name: f.name,\n label: f.label,\n type: f.type,\n ...(f.order ? { order: f.order } : {}),\n }))\n\n/**\n * An engine that sums up in a worker, so a million records never hold up\n * the page. The page reads each field the config needs once, in slices of\n * a few milliseconds, and moves it to the worker (no copy); after that\n * only configs go over.\n *\n * Pass a worker that calls `servePivot`, or a function that makes one\n * (made when first needed).\n */\nexport function createPivotWorker(worker: PivotWorkerLike | (() => PivotWorkerLike)): PivotEngine {\n let w: PivotWorkerLike | null = null\n let current: object | null = null\n let sourceId = 0\n let nextId = 0\n /** Why the worker stopped working, once it has. */\n let broken: string | null = null\n const known = new Set<string>()\n const sent = { dimensions: new Set<string>(), measures: new Set<string>() }\n const waiting = new Map<\n number,\n {\n resolve: (message: PivotFromWorker) => void\n }\n >()\n\n // one compute reads fields at a time, so two never read the same field\n let lane: Promise<unknown> = Promise.resolve()\n\n const post = (message: PivotToWorker, transfer?: Transferable[]) =>\n w!.postMessage(message, transfer)\n\n const connect = () => {\n if (w) return w\n w = typeof worker === 'function' ? worker() : worker\n w.addEventListener('message', (event) => {\n const m = event.data as PivotFromWorker\n const job = waiting.get(m.id)\n if (!job) return\n waiting.delete(m.id)\n job.resolve(m)\n })\n w.addEventListener('error', (event) => {\n broken = brokenMessage((event as ErrorEvent).message)\n for (const job of waiting.values()) job.resolve({ type: 'error', id: -1, message: broken })\n waiting.clear()\n })\n return w\n }\n\n /** Reads and sends the fields the worker does not have yet. */\n async function ship<Row>(\n source: PivotSource<Row>,\n need: { dimensions: string[]; measures: string[] },\n signal?: AbortSignal,\n ) {\n const fresh = describe(source).filter((f) => !known.has(f.name))\n if (fresh.length) {\n for (const f of fresh) known.add(f.name)\n post({ type: 'fields', source: sourceId, fields: fresh })\n }\n for (const name of need.dimensions) {\n if (sent.dimensions.has(name)) continue\n const id = sourceId\n const column = await sliced(source.encodeSteps(name), signal)\n if (id !== sourceId) throw abortError()\n post({ type: 'dimension', source: id, name, ...column }, [column.codes.buffer])\n sent.dimensions.add(name)\n }\n for (const name of need.measures) {\n if (sent.measures.has(name)) continue\n const id = sourceId\n const values = await sliced(source.measureSteps(name, true), signal)\n if (id !== sourceId) throw abortError()\n post({ type: 'measure', source: id, name, values }, [values.buffer])\n sent.measures.add(name)\n }\n }\n\n return {\n async compute(source, config: PivotConfig, request: PivotComputeRequest = {}) {\n const { signal } = request\n try {\n connect()\n } catch (error) {\n broken = brokenMessage((error as Error).message)\n }\n if (broken) throw new Error(broken)\n if (source !== current) {\n current = source\n sourceId++\n known.clear()\n sent.dimensions.clear()\n sent.measures.clear()\n const fields = describe(source)\n for (const f of fields) known.add(f.name)\n post({\n type: 'source',\n source: sourceId,\n length: source.length,\n locale: source.locale,\n fields,\n })\n }\n const need = neededColumns(source, config, request.aggregators)\n const shipped = lane.then(() => ship(source, need, signal))\n lane = shipped.catch(() => {})\n await shipped\n // the worker may need more than the page could tell (a custom aggregator's input)\n for (let round = 0; round < 3; round++) {\n if (signal?.aborted) throw abortError()\n const id = nextId++\n const answer = await new Promise<PivotFromWorker>((resolve) => {\n if (broken) return resolve({ type: 'error', id, message: broken })\n waiting.set(id, { resolve })\n post({ type: 'compute', source: sourceId, id, config })\n })\n if (answer.type === 'result') return PivotResult.unpack(answer.packet, source, config)\n if (answer.type === 'error') throw new Error(answer.message)\n if (answer.type === 'skipped') throw abortError()\n await ship(source, answer, signal)\n }\n throw new Error('[@dou-ui/pivot] the worker still lacks fields after they were sent')\n },\n dispose() {\n w?.terminate?.()\n w = null\n current = null\n for (const job of waiting.values()) job.resolve({ type: 'skipped', id: -1 })\n waiting.clear()\n },\n }\n}\n"],"mappings":"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"}