Skip to content

Commit 52a1f0a

Browse files
committed
first draft of dataframeV2
1 parent 4b85b09 commit 52a1f0a

File tree

2 files changed

+151
-0
lines changed

2 files changed

+151
-0
lines changed

src/helpers/dataframeV2.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
}

src/helpers/typedEventTarget.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// EventTarget is not a generic type in TypeScript.
2+
// We use the code adapted from https://github.com/microsoft/TypeScript/issues/28357#issuecomment-2310186394
3+
// to add type safety.
4+
5+
/**
6+
* Type-safe event listener and dispatch signatures for the custom events
7+
* defined in `TDetails`.
8+
*/
9+
export interface CustomEventTarget<TDetails> {
10+
addEventListener<TType extends keyof TDetails>(
11+
type: TType,
12+
13+
listener: (ev: CustomEvent<TDetails[TType]>) => any,
14+
options?: boolean | AddEventListenerOptions,
15+
): void;
16+
17+
removeEventListener<TType extends keyof TDetails>(
18+
type: TType,
19+
20+
listener: (ev: CustomEvent<TDetails[TType]>) => any,
21+
options?: boolean | EventListenerOptions,
22+
): void;
23+
24+
dispatchEvent<TType extends keyof TDetails>(
25+
ev: _TypedCustomEvent<TDetails, TType>,
26+
): void;
27+
}
28+
29+
/**
30+
* Internal declaration for the `typeof` trick below.
31+
* Never actually implemented.
32+
*/
33+
declare class _TypedCustomEvent<
34+
TDetails,
35+
TType extends keyof TDetails,
36+
> extends CustomEvent<TDetails[TType]> {
37+
constructor(
38+
type: TType,
39+
eventInitDict: { detail: TDetails[TType] } & EventInit,
40+
);
41+
}
42+
43+
/**
44+
* Typed custom event (technically a typed alias of `CustomEvent`).
45+
* Use with `CustomEventTarget.dispatchEvent` to infer `detail` types
46+
* automatically.
47+
*/
48+
export const TypedCustomEvent = CustomEvent as typeof _TypedCustomEvent
49+
50+
export function createEventTarget<TDetails>(): CustomEventTarget<TDetails> {
51+
return new EventTarget() as CustomEventTarget<TDetails>
52+
}

0 commit comments

Comments
 (0)