|
| 1 | +import { Cells } from './row.js' |
| 2 | +import { OrderBy } from './sort' |
| 3 | +import { CustomEventTarget, createEventTarget } from './typedEventTarget' |
| 4 | + |
| 5 | +// Map of event type -> detail |
| 6 | +// Starting with a single event, required in Iceberg |
| 7 | +// TODO(SL): shall we force lowercase event type? https://developer.mozilla.org/en-US/docs/Web/API/Element/MozMousePixelScroll_event is a counter-example (but deprecated). |
| 8 | +interface DataFrameEvents { |
| 9 | + 'dataframe:numrowschange': { numRows: number }; |
| 10 | +} |
| 11 | + |
| 12 | +/** |
| 13 | + * DataFrameV2 is an interface for a data structure that represents a table of data. |
| 14 | + * |
| 15 | + * It can mutate its data, and a table can subscribe to changes using the eventTarget. |
| 16 | + */ |
| 17 | +export interface DataFrameV2 { |
| 18 | + numRows: number |
| 19 | + header: string[] |
| 20 | + |
| 21 | + // undefined means pending, ResolvedValue is a boxed value type (so we can distinguish undefined from pending) |
| 22 | + // getCell does NOT initiate a fetch, it just returns resolved data |
| 23 | + getCell({ row, column, orderBy }: {row: number, column: string, orderBy?: OrderBy}): ResolvedValue | undefined |
| 24 | + |
| 25 | + // initiate fetches for row/column data: |
| 26 | + fetch({ rowStart, rowEnd, columns, orderBy }: { rowStart: number, rowEnd: number, columns: string[], orderBy?: OrderBy }): CancellableJob |
| 27 | + |
| 28 | + // emits events, defined in DataFrameEvents |
| 29 | + eventTarget: CustomEventTarget<DataFrameEvents> |
| 30 | +} |
| 31 | + |
| 32 | +interface ResolvedValue { |
| 33 | + value: any |
| 34 | +} |
| 35 | + |
| 36 | +interface CancellableJob { |
| 37 | + // table can call cancel when a user scrolls out of view. dataframe implementer can choose to ignore, de-queue, or cancel in flight fetches. |
| 38 | + cancel(): void |
| 39 | +} |
| 40 | + |
| 41 | +export function arrayDataFrame(data: Cells[]): DataFrameV2 { |
| 42 | + const eventTarget = createEventTarget<DataFrameEvents>() |
| 43 | + // eventTarget can be used as follows: |
| 44 | + // |
| 45 | + // listen to an event: |
| 46 | + // eventTarget.addEventListener('dataframe:numrowschange', (event) => { |
| 47 | + // console.log('Number of rows changed:', event.detail.numRows) |
| 48 | + // }) |
| 49 | + // |
| 50 | + // publish an event: |
| 51 | + // eventTarget.dispatchEvent(new CustomEvent('dataframe:numrowschange', { detail: { numRows: 42 } })) |
| 52 | + |
| 53 | + function fetch({ orderBy }: { rowStart: number, rowEnd: number, columns: string[], orderBy?: OrderBy }) { |
| 54 | + if (orderBy && orderBy.length > 0) { |
| 55 | + throw new Error('Sorting is not implemented.') |
| 56 | + } |
| 57 | + return { |
| 58 | + cancel: () => { |
| 59 | + // No-op for static data |
| 60 | + }, |
| 61 | + } |
| 62 | + } |
| 63 | + if (!(0 in data)) { |
| 64 | + return { |
| 65 | + numRows: 0, |
| 66 | + header: [], |
| 67 | + getCell: () => undefined, |
| 68 | + fetch, |
| 69 | + eventTarget, |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + const header = Object.keys(data[0]) |
| 74 | + |
| 75 | + return { |
| 76 | + numRows: data.length, |
| 77 | + header, |
| 78 | + getCell: ({ row, column, orderBy }) => { |
| 79 | + if (orderBy && orderBy.length > 0) { |
| 80 | + throw new Error('Sorting is not implemented.') |
| 81 | + } |
| 82 | + const cells = data[row] |
| 83 | + if (!cells) { |
| 84 | + throw new Error(`Invalid row index: ${row}`) |
| 85 | + } |
| 86 | + if (!header.includes(column)) { |
| 87 | + throw new Error(`Invalid column: ${column}`) |
| 88 | + } |
| 89 | + if (!(column in cells)) { |
| 90 | + throw new Error(`Column "${column}" not found in row ${row}`) |
| 91 | + } |
| 92 | + // Return a resolved value (which might be undefined as well) |
| 93 | + return { value: data[row] } |
| 94 | + // Note that this function never returns undefined (meaning pending cell), because the data is static. |
| 95 | + }, |
| 96 | + fetch, |
| 97 | + eventTarget, |
| 98 | + } |
| 99 | +} |
0 commit comments