Split DataSource & DataSourceView
Summary: This diff is primarily cosmetic, just pushing code around to make the API more intuitive. Most importantly, DataSource was split into DataSource and DataSourceView classes, the latter being accessible through `datasource.view`. The benefit of this is two fold: 1. Conceptually it is much clearer now which operations operate on the _source_ records, and which ones on the derived _view_. 2. This will make it easier in the future to support multiple views to be based on a single data source. This refactoring also nicely found 2 cases where datasource logic and view logic were mixed. The only semantic change in this diff is that both DataSource and DataSourceView are now iterable, so that one can do a `for (const record of ds)` / `for (const record of ds.view)` Reviewed By: nikoant Differential Revision: D26976838 fbshipit-source-id: 3726e92b3c6ee3417dc66cbbe6e288797eecf70e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d73f6578a7
commit
602152665b
@@ -577,7 +577,7 @@ test('plugins can serialize dataSources', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(instance.ds.records).toEqual([4, 5]);
|
expect(instance.ds.records()).toEqual([4, 5]);
|
||||||
instance.ds.shift(1);
|
instance.ds.shift(1);
|
||||||
instance.ds.append(6);
|
instance.ds.append(6);
|
||||||
expect(exportState()).toEqual({
|
expect(exportState()).toEqual({
|
||||||
|
|||||||
@@ -95,132 +95,93 @@ export class DataSource<
|
|||||||
> implements Persistable {
|
> implements Persistable {
|
||||||
private nextId = 0;
|
private nextId = 0;
|
||||||
private _records: Entry<T>[] = [];
|
private _records: Entry<T>[] = [];
|
||||||
|
|
||||||
private _recordsById: Map<KEY_TYPE, T> = new Map();
|
private _recordsById: Map<KEY_TYPE, T> = new Map();
|
||||||
private keyAttribute: undefined | keyof T;
|
private keyAttribute: undefined | keyof T;
|
||||||
private idToIndex: Map<KEY_TYPE, number> = new Map();
|
private idToIndex: Map<KEY_TYPE, number> = new Map();
|
||||||
// if we shift the window, we increase shiftOffset, rather than remapping all values
|
|
||||||
|
// if we shift the window, we increase shiftOffset to correct idToIndex results, rather than remapping all values
|
||||||
private shiftOffset = 0;
|
private shiftOffset = 0;
|
||||||
limit = defaultLimit;
|
|
||||||
|
|
||||||
private sortBy: undefined | ((a: T) => Primitive);
|
|
||||||
|
|
||||||
private reverse: boolean = false;
|
|
||||||
|
|
||||||
private filter?: (value: T) => boolean;
|
|
||||||
|
|
||||||
private dataUpdateQueue: DataEvent<T>[] = [];
|
|
||||||
|
|
||||||
windowStart = 0;
|
|
||||||
windowEnd = 0;
|
|
||||||
|
|
||||||
private outputChangeListener?: (change: OutputChange) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposed for testing.
|
* The maximum amount of records this DataSource can have
|
||||||
* This is the base view data, that is filtered and sorted, but not reversed or windowed
|
|
||||||
*/
|
*/
|
||||||
output: Entry<T>[] = [];
|
public limit = defaultLimit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a defensive copy of the stored records.
|
* The default view on this data source. A view applies
|
||||||
* This is a O(n) operation! Prefer using .size and .get instead!
|
* sorting, filtering and windowing to get more constrained output.
|
||||||
|
*
|
||||||
|
* Additional views can created through the fork method.
|
||||||
*/
|
*/
|
||||||
get records(): readonly T[] {
|
public readonly view: DataSourceView<T>;
|
||||||
return this._records.map(unwrap);
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize() {
|
|
||||||
return this.records;
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(value: any[]) {
|
|
||||||
this.clear();
|
|
||||||
value.forEach((record) => {
|
|
||||||
this.append(record);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a direct reference to the stored records as lookup map,
|
|
||||||
* based on the key attribute set.
|
|
||||||
* The colletion should be treated as readonly and mutable (it might change over time).
|
|
||||||
* Create a defensive copy if needed.
|
|
||||||
*/
|
|
||||||
get recordsById(): ReadonlyMap<KEY_TYPE, T> {
|
|
||||||
this.assertKeySet();
|
|
||||||
return this._recordsById;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(keyAttribute: KEY | undefined) {
|
constructor(keyAttribute: KEY | undefined) {
|
||||||
this.keyAttribute = keyAttribute;
|
this.keyAttribute = keyAttribute;
|
||||||
this.setSortBy(undefined);
|
this.view = new DataSourceView<T>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get size() {
|
public get size() {
|
||||||
return this._records.length;
|
return this._records.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecord(index: number): T {
|
|
||||||
return this._records[index]?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get outputSize() {
|
|
||||||
return this.output.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a defensive copy of the current output.
|
* Returns a defensive copy of the stored records.
|
||||||
* Sort, filter, reverse and are applied.
|
* This is a O(n) operation! Prefer using .size and .get instead if only a subset is needed.
|
||||||
* Start and end behave like slice, and default to the currently active window.
|
|
||||||
*/
|
*/
|
||||||
public getOutput(
|
public records(): readonly T[] {
|
||||||
start = this.windowStart,
|
return this._records.map(unwrap);
|
||||||
end = this.windowEnd,
|
|
||||||
): readonly T[] {
|
|
||||||
if (this.reverse) {
|
|
||||||
return this.output
|
|
||||||
.slice(this.output.length - end, this.output.length - start)
|
|
||||||
.reverse()
|
|
||||||
.map((e) => e.value);
|
|
||||||
} else {
|
|
||||||
return this.output.slice(start, end).map((e) => e.value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertKeySet() {
|
public get(index: number) {
|
||||||
if (!this.keyAttribute) {
|
return unwrap(this._records[index]);
|
||||||
throw new Error(
|
|
||||||
'No key has been set. Records cannot be looked up by key',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(value: T): KEY_TYPE;
|
public getById(key: KEY_TYPE) {
|
||||||
private getKey(value: any): any {
|
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const key = value[this.keyAttribute!];
|
return this._recordsById.get(key);
|
||||||
if ((typeof key === 'string' || typeof key === 'number') && key !== '') {
|
}
|
||||||
return key;
|
|
||||||
}
|
public keys(): IterableIterator<KEY_TYPE> {
|
||||||
throw new Error(`Invalid key value: '${key}'`);
|
this.assertKeySet();
|
||||||
|
return this._recordsById.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public entries(): IterableIterator<[KEY_TYPE, T]> {
|
||||||
|
this.assertKeySet();
|
||||||
|
return this._recordsById.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public [Symbol.iterator](): IterableIterator<T> {
|
||||||
|
const self = this;
|
||||||
|
let offset = 0;
|
||||||
|
return {
|
||||||
|
next() {
|
||||||
|
offset++;
|
||||||
|
if (offset > self.size) {
|
||||||
|
return {done: true, value: undefined};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
value: self._records[offset - 1].value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of a specific key in the *source* set
|
* Returns the index of a specific key in the *records* set.
|
||||||
|
* Returns -1 if the record wansn't found
|
||||||
*/
|
*/
|
||||||
indexOfKey(key: KEY_TYPE): number {
|
public getIndexOfKey(key: KEY_TYPE): number {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const stored = this.idToIndex.get(key);
|
const stored = this.idToIndex.get(key);
|
||||||
return stored === undefined ? -1 : stored + this.shiftOffset;
|
return stored === undefined ? -1 : stored + this.shiftOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeIndexOfKey(key: KEY_TYPE, index: number) {
|
public append(value: T) {
|
||||||
// de-normalize the index, so that on later look ups its corrected again
|
|
||||||
this.idToIndex.set(key, index - this.shiftOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
append(value: T) {
|
|
||||||
if (this._records.length >= this.limit) {
|
if (this._records.length >= this.limit) {
|
||||||
// we're full! let's free up some space
|
// we're full! let's free up some space
|
||||||
this.shift(Math.ceil(this.limit * dropFactor));
|
this.shift(Math.ceil(this.limit * dropFactor));
|
||||||
@@ -236,7 +197,8 @@ export class DataSource<
|
|||||||
const entry = {
|
const entry = {
|
||||||
value,
|
value,
|
||||||
id: ++this.nextId,
|
id: ++this.nextId,
|
||||||
visible: this.filter ? this.filter(value) : true,
|
// once we have multiple views, the following fields should be stored per view
|
||||||
|
visible: true,
|
||||||
approxIndex: -1,
|
approxIndex: -1,
|
||||||
};
|
};
|
||||||
this._records.push(entry);
|
this._records.push(entry);
|
||||||
@@ -250,11 +212,11 @@ export class DataSource<
|
|||||||
* Updates or adds a record. Returns `true` if the record already existed.
|
* Updates or adds a record. Returns `true` if the record already existed.
|
||||||
* Can only be used if a key is used.
|
* Can only be used if a key is used.
|
||||||
*/
|
*/
|
||||||
upsert(value: T): boolean {
|
public upsert(value: T): boolean {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const key = this.getKey(value);
|
const key = this.getKey(value);
|
||||||
if (this.idToIndex.has(key)) {
|
if (this.idToIndex.has(key)) {
|
||||||
this.update(this.indexOfKey(key), value);
|
this.update(this.getIndexOfKey(key), value);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.append(value);
|
this.append(value);
|
||||||
@@ -266,7 +228,7 @@ export class DataSource<
|
|||||||
* Replaces an item in the base data collection.
|
* Replaces an item in the base data collection.
|
||||||
* Note that the index is based on the insertion order, and not based on the current view
|
* Note that the index is based on the insertion order, and not based on the current view
|
||||||
*/
|
*/
|
||||||
update(index: number, value: T) {
|
public update(index: number, value: T) {
|
||||||
const entry = this._records[index];
|
const entry = this._records[index];
|
||||||
const oldValue = entry.value;
|
const oldValue = entry.value;
|
||||||
if (value === oldValue) {
|
if (value === oldValue) {
|
||||||
@@ -274,11 +236,16 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
const oldVisible = entry.visible;
|
const oldVisible = entry.visible;
|
||||||
entry.value = value;
|
entry.value = value;
|
||||||
entry.visible = this.filter ? this.filter(value) : true;
|
|
||||||
if (this.keyAttribute) {
|
if (this.keyAttribute) {
|
||||||
const key = this.getKey(value);
|
const key = this.getKey(value);
|
||||||
const currentKey = this.getKey(oldValue);
|
const currentKey = this.getKey(oldValue);
|
||||||
if (currentKey !== key) {
|
if (currentKey !== key) {
|
||||||
|
const existingIndex = this.getIndexOfKey(key);
|
||||||
|
if (existingIndex !== -1 && existingIndex !== index) {
|
||||||
|
throw new Error(
|
||||||
|
`Trying to insert duplicate key '${key}', which already exist in the collection`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this._recordsById.delete(currentKey);
|
this._recordsById.delete(currentKey);
|
||||||
this.idToIndex.delete(currentKey);
|
this.idToIndex.delete(currentKey);
|
||||||
}
|
}
|
||||||
@@ -299,7 +266,7 @@ export class DataSource<
|
|||||||
*
|
*
|
||||||
* Warning: this operation can be O(n) if a key is set
|
* Warning: this operation can be O(n) if a key is set
|
||||||
*/
|
*/
|
||||||
remove(index: number) {
|
public delete(index: number) {
|
||||||
if (index < 0 || index >= this._records.length) {
|
if (index < 0 || index >= this._records.length) {
|
||||||
throw new Error('Out of bounds: ' + index);
|
throw new Error('Out of bounds: ' + index);
|
||||||
}
|
}
|
||||||
@@ -332,13 +299,13 @@ export class DataSource<
|
|||||||
*
|
*
|
||||||
* Warning: this operation can be O(n) if a key is set
|
* Warning: this operation can be O(n) if a key is set
|
||||||
*/
|
*/
|
||||||
removeByKey(keyValue: KEY_TYPE): boolean {
|
public deleteByKey(keyValue: KEY_TYPE): boolean {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const index = this.indexOfKey(keyValue);
|
const index = this.getIndexOfKey(keyValue);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.remove(index);
|
this.delete(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +313,7 @@ export class DataSource<
|
|||||||
* Removes the first N entries.
|
* Removes the first N entries.
|
||||||
* @param amount
|
* @param amount
|
||||||
*/
|
*/
|
||||||
shift(amount: number) {
|
public shift(amount: number) {
|
||||||
amount = Math.min(amount, this._records.length);
|
amount = Math.min(amount, this._records.length);
|
||||||
if (amount === this._records.length) {
|
if (amount === this._records.length) {
|
||||||
this.clear();
|
this.clear();
|
||||||
@@ -365,7 +332,7 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.sortBy &&
|
this.view.isSorted &&
|
||||||
removed.length > 10 &&
|
removed.length > 10 &&
|
||||||
removed.length > shiftRebuildTreshold * this._records.length
|
removed.length > shiftRebuildTreshold * this._records.length
|
||||||
) {
|
) {
|
||||||
@@ -373,7 +340,7 @@ export class DataSource<
|
|||||||
// let's fallback to the async processing of all data instead
|
// let's fallback to the async processing of all data instead
|
||||||
// MWE: there is a risk here that rebuilding is too blocking, as this might happen
|
// MWE: there is a risk here that rebuilding is too blocking, as this might happen
|
||||||
// in background when new data arrives, and not explicitly on a user interaction
|
// in background when new data arrives, and not explicitly on a user interaction
|
||||||
this.rebuildOutput();
|
this.view.rebuild();
|
||||||
} else {
|
} else {
|
||||||
this.emitDataEvent({
|
this.emitDataEvent({
|
||||||
type: 'shift',
|
type: 'shift',
|
||||||
@@ -383,13 +350,182 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setWindow(start: number, end: number) {
|
/**
|
||||||
|
* The clear operation removes any records stored, but will keep the current view preferences such as sorting and filtering
|
||||||
|
*/
|
||||||
|
public clear() {
|
||||||
|
this._records = [];
|
||||||
|
this._recordsById = new Map();
|
||||||
|
this.shiftOffset = 0;
|
||||||
|
this.idToIndex = new Map();
|
||||||
|
this.view.rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a fork of this dataSource, that shares the source data with this dataSource,
|
||||||
|
* but has it's own FSRW pipeline, to allow multiple views on the same data
|
||||||
|
*/
|
||||||
|
public fork(): DataSourceView<T> {
|
||||||
|
throw new Error(
|
||||||
|
'Not implemented. Please contact oncall if this feature is needed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertKeySet() {
|
||||||
|
if (!this.keyAttribute) {
|
||||||
|
throw new Error(
|
||||||
|
'No key has been set. Records cannot be looked up by key',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKey(value: T): KEY_TYPE;
|
||||||
|
private getKey(value: any): any {
|
||||||
|
this.assertKeySet();
|
||||||
|
const key = value[this.keyAttribute!];
|
||||||
|
if ((typeof key === 'string' || typeof key === 'number') && key !== '') {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid key value: '${key}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private storeIndexOfKey(key: KEY_TYPE, index: number) {
|
||||||
|
// de-normalize the index, so that on later look ups its corrected again
|
||||||
|
this.idToIndex.set(key, index - this.shiftOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitDataEvent(event: DataEvent<T>) {
|
||||||
|
// Optimization: potentially we could schedule this to happen async,
|
||||||
|
// using a queue,
|
||||||
|
// or only if there is an active view (although that could leak memory)
|
||||||
|
this.view.processEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
serialize(): readonly T[] {
|
||||||
|
return this.records();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
deserialize(value: any[]) {
|
||||||
|
this.clear();
|
||||||
|
value.forEach((record) => {
|
||||||
|
this.append(record);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDataSourceOptions<T, K extends keyof T> = {
|
||||||
|
/**
|
||||||
|
* If a key is set, the given field of the records is assumed to be unique,
|
||||||
|
* and it's value can be used to perform lookups and upserts.
|
||||||
|
*/
|
||||||
|
key?: K;
|
||||||
|
/**
|
||||||
|
* The maximum amount of records that this DataSource will store.
|
||||||
|
* If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
/**
|
||||||
|
* Should this state persist when exporting a plugin?
|
||||||
|
* If set, the dataSource will be saved / loaded under the key provided
|
||||||
|
*/
|
||||||
|
persist?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDataSource<T, KEY extends keyof T = any>(
|
||||||
|
initialSet: T[],
|
||||||
|
options: CreateDataSourceOptions<T, KEY>,
|
||||||
|
): DataSource<T, KEY, ExtractKeyType<T, KEY>>;
|
||||||
|
export function createDataSource<T>(
|
||||||
|
initialSet?: T[],
|
||||||
|
): DataSource<T, never, never>;
|
||||||
|
export function createDataSource<T, KEY extends keyof T>(
|
||||||
|
initialSet: T[] = [],
|
||||||
|
options?: CreateDataSourceOptions<T, KEY>,
|
||||||
|
): DataSource<T, any, any> {
|
||||||
|
const ds = new DataSource<T, KEY>(options?.key);
|
||||||
|
if (options?.limit !== undefined) {
|
||||||
|
ds.limit = options.limit;
|
||||||
|
}
|
||||||
|
registerStorageAtom(options?.persist, ds);
|
||||||
|
initialSet.forEach((value) => ds.append(value));
|
||||||
|
return ds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrap<T>(entry: Entry<T>): T {
|
||||||
|
return entry?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataSourceView<T> {
|
||||||
|
public readonly datasource: DataSource<T>;
|
||||||
|
private sortBy: undefined | ((a: T) => Primitive) = undefined;
|
||||||
|
private reverse: boolean = false;
|
||||||
|
private filter?: (value: T) => boolean = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
public windowStart = 0;
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
public windowEnd = 0;
|
||||||
|
|
||||||
|
private outputChangeListener?: (change: OutputChange) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the base view data, that is filtered and sorted, but not reversed or windowed
|
||||||
|
*/
|
||||||
|
private _output: Entry<T>[] = [];
|
||||||
|
|
||||||
|
constructor(datasource: DataSource<T, any, any>) {
|
||||||
|
this.datasource = datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size() {
|
||||||
|
return this._output.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isSorted() {
|
||||||
|
return !!this.sortBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isFiltered() {
|
||||||
|
return !!this.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isReversed() {
|
||||||
|
return this.reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a defensive copy of the current output.
|
||||||
|
* Sort, filter, reverse and are applied.
|
||||||
|
* Start and end behave like slice, and default to the currently active window.
|
||||||
|
*/
|
||||||
|
public output(start = this.windowStart, end = this.windowEnd): readonly T[] {
|
||||||
|
if (this.reverse) {
|
||||||
|
return this._output
|
||||||
|
.slice(this._output.length - end, this._output.length - start)
|
||||||
|
.reverse()
|
||||||
|
.map((e) => e.value);
|
||||||
|
} else {
|
||||||
|
return this._output.slice(start, end).map((e) => e.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setWindow(start: number, end: number) {
|
||||||
this.windowStart = start;
|
this.windowStart = start;
|
||||||
this.windowEnd = end;
|
this.windowEnd = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutputChangeListener(
|
public setListener(
|
||||||
listener: typeof DataSource['prototype']['outputChangeListener'],
|
listener: typeof DataSourceView['prototype']['outputChangeListener'],
|
||||||
) {
|
) {
|
||||||
if (this.outputChangeListener && listener) {
|
if (this.outputChangeListener && listener) {
|
||||||
console.warn('outputChangeListener already set');
|
console.warn('outputChangeListener already set');
|
||||||
@@ -397,7 +533,7 @@ export class DataSource<
|
|||||||
this.outputChangeListener = listener;
|
this.outputChangeListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSortBy(sortBy: undefined | keyof T | ((a: T) => Primitive)) {
|
public setSortBy(sortBy: undefined | keyof T | ((a: T) => Primitive)) {
|
||||||
if (this.sortBy === sortBy) {
|
if (this.sortBy === sortBy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -411,42 +547,27 @@ export class DataSource<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.sortBy = sortBy as any;
|
this.sortBy = sortBy as any;
|
||||||
this.rebuildOutput();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(filter: undefined | ((value: T) => boolean)) {
|
public setFilter(filter: undefined | ((value: T) => boolean)) {
|
||||||
if (this.filter !== filter) {
|
if (this.filter !== filter) {
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
this.rebuildOutput();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleReversed() {
|
public toggleReversed() {
|
||||||
this.setReversed(!this.reverse);
|
this.setReversed(!this.reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReversed(reverse: boolean) {
|
public setReversed(reverse: boolean) {
|
||||||
if (this.reverse !== reverse) {
|
if (this.reverse !== reverse) {
|
||||||
this.reverse = reverse;
|
this.reverse = reverse;
|
||||||
this.notifyReset(this.output.length);
|
this.notifyReset(this._output.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The clear operation removes any records stored, but will keep the current view preferences such as sorting and filtering
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.windowStart = 0;
|
|
||||||
this.windowEnd = 0;
|
|
||||||
this._records = [];
|
|
||||||
this._recordsById = new Map();
|
|
||||||
this.shiftOffset = 0;
|
|
||||||
this.idToIndex = new Map();
|
|
||||||
this.dataUpdateQueue = [];
|
|
||||||
this.output = [];
|
|
||||||
this.notifyReset(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The reset operation resets any view preferences such as sorting and filtering, but keeps the current set of records.
|
* The reset operation resets any view preferences such as sorting and filtering, but keeps the current set of records.
|
||||||
*/
|
*/
|
||||||
@@ -454,35 +575,37 @@ export class DataSource<
|
|||||||
this.sortBy = undefined;
|
this.sortBy = undefined;
|
||||||
this.reverse = false;
|
this.reverse = false;
|
||||||
this.filter = undefined;
|
this.filter = undefined;
|
||||||
this.rebuildOutput();
|
this.windowStart = 0;
|
||||||
}
|
this.windowEnd = 0;
|
||||||
|
this.rebuild();
|
||||||
/**
|
|
||||||
* Returns a fork of this dataSource, that shares the source data with this dataSource,
|
|
||||||
* but has it's own FSRW pipeline, to allow multiple views on the same data
|
|
||||||
*/
|
|
||||||
fork(): DataSource<T> {
|
|
||||||
throw new Error(
|
|
||||||
'Not implemented. Please contact oncall if this feature is needed',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitDataEvent(event: DataEvent<T>) {
|
|
||||||
this.dataUpdateQueue.push(event);
|
|
||||||
// TODO: schedule
|
|
||||||
this.processEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeIndex(viewIndex: number): number {
|
private normalizeIndex(viewIndex: number): number {
|
||||||
return this.reverse ? this.output.length - 1 - viewIndex : viewIndex;
|
return this.reverse ? this._output.length - 1 - viewIndex : viewIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(viewIndex: number): T {
|
public get(viewIndex: number): T {
|
||||||
return this.getEntry(viewIndex)?.value;
|
return this._output[this.normalizeIndex(viewIndex)]?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntry(viewIndex: number): Entry<T> {
|
public [Symbol.iterator](): IterableIterator<T> {
|
||||||
return this.output[this.normalizeIndex(viewIndex)];
|
const self = this;
|
||||||
|
let offset = this.windowStart;
|
||||||
|
return {
|
||||||
|
next() {
|
||||||
|
offset++;
|
||||||
|
if (offset > self.windowEnd || offset > self.size) {
|
||||||
|
return {done: true, value: undefined};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
value: self.get(offset - 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyItemUpdated(viewIndex: number) {
|
private notifyItemUpdated(viewIndex: number) {
|
||||||
@@ -508,12 +631,12 @@ export class DataSource<
|
|||||||
if (this.reverse && delta < 0) {
|
if (this.reverse && delta < 0) {
|
||||||
viewIndex -= delta; // we need to correct for normalize already using the new length after applying this change
|
viewIndex -= delta; // we need to correct for normalize already using the new length after applying this change
|
||||||
}
|
}
|
||||||
// TODO: for 'before' shifts, should the window be adjusted automatically?
|
// Idea: we could add an option to automatically shift the window for before events.
|
||||||
this.outputChangeListener({
|
this.outputChangeListener({
|
||||||
type: 'shift',
|
type: 'shift',
|
||||||
delta,
|
delta,
|
||||||
index: viewIndex,
|
index: viewIndex,
|
||||||
newCount: this.output.length,
|
newCount: this._output.length,
|
||||||
location:
|
location:
|
||||||
viewIndex < this.windowStart
|
viewIndex < this.windowStart
|
||||||
? 'before'
|
? 'before'
|
||||||
@@ -530,16 +653,15 @@ export class DataSource<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private processEvents() {
|
/**
|
||||||
const events = this.dataUpdateQueue.splice(0);
|
* @private
|
||||||
events.forEach(this.processEvent);
|
*/
|
||||||
}
|
processEvent(event: DataEvent<T>) {
|
||||||
|
const {_output: output, sortBy, filter} = this;
|
||||||
private processEvent = (event: DataEvent<T>) => {
|
|
||||||
const {output, sortBy, filter} = this;
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'append': {
|
case 'append': {
|
||||||
const {entry} = event;
|
const {entry} = event;
|
||||||
|
entry.visible = filter ? filter(entry.value) : true;
|
||||||
if (!entry.visible) {
|
if (!entry.visible) {
|
||||||
// not in filter? skip this entry
|
// not in filter? skip this entry
|
||||||
return;
|
return;
|
||||||
@@ -556,6 +678,7 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const {entry} = event;
|
const {entry} = event;
|
||||||
|
entry.visible = filter ? filter(entry.value) : true;
|
||||||
// short circuit; no view active so update straight away
|
// short circuit; no view active so update straight away
|
||||||
if (!filter && !sortBy) {
|
if (!filter && !sortBy) {
|
||||||
output[event.index].approxIndex = event.index;
|
output[event.index].approxIndex = event.index;
|
||||||
@@ -624,10 +747,10 @@ export class DataSource<
|
|||||||
default:
|
default:
|
||||||
throw new Error('unknown event type');
|
throw new Error('unknown event type');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
private processRemoveEvent(index: number, entry: Entry<T>) {
|
private processRemoveEvent(index: number, entry: Entry<T>) {
|
||||||
const {output, sortBy, filter} = this;
|
const {_output: output, sortBy, filter} = this;
|
||||||
|
|
||||||
// filter active, and not visible? short circuilt
|
// filter active, and not visible? short circuilt
|
||||||
if (!entry.visible) {
|
if (!entry.visible) {
|
||||||
@@ -645,7 +768,11 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildOutput() {
|
/**
|
||||||
|
* Rebuilds the entire view. Typically there should be no need to call this manually
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
rebuild() {
|
||||||
// Pending on the size, should we batch this in smaller non-blocking steps,
|
// Pending on the size, should we batch this in smaller non-blocking steps,
|
||||||
// which we update in a double-buffering mechanism, report progress, and swap out when done?
|
// which we update in a double-buffering mechanism, report progress, and swap out when done?
|
||||||
//
|
//
|
||||||
@@ -654,12 +781,14 @@ export class DataSource<
|
|||||||
// See also comment below
|
// See also comment below
|
||||||
const {sortBy, filter, sortHelper} = this;
|
const {sortBy, filter, sortHelper} = this;
|
||||||
// copy base array or run filter (with side effecty update of visible)
|
// copy base array or run filter (with side effecty update of visible)
|
||||||
|
// @ts-ignore prevent making _record public
|
||||||
|
const records: Entry<T>[] = this.datasource._records;
|
||||||
let output = filter
|
let output = filter
|
||||||
? this._records.filter((entry) => {
|
? records.filter((entry) => {
|
||||||
entry.visible = filter(entry.value);
|
entry.visible = filter(entry.value);
|
||||||
return entry.visible;
|
return entry.visible;
|
||||||
})
|
})
|
||||||
: this._records.slice();
|
: records.slice();
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
// Pending on the size, should we batch this in smaller steps?
|
// Pending on the size, should we batch this in smaller steps?
|
||||||
// The following sorthing method can be taskified, however,
|
// The following sorthing method can be taskified, however,
|
||||||
@@ -674,7 +803,7 @@ export class DataSource<
|
|||||||
output = lodashSort(output, sortHelper); // uses array.sort under the hood
|
output = lodashSort(output, sortHelper); // uses array.sort under the hood
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output = output;
|
this._output = output;
|
||||||
this.notifyReset(output.length);
|
this.notifyReset(output.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +811,7 @@ export class DataSource<
|
|||||||
this.sortBy ? this.sortBy(a.value) : a.id;
|
this.sortBy ? this.sortBy(a.value) : a.id;
|
||||||
|
|
||||||
private getSortedIndex(entry: Entry<T>, oldValue: T) {
|
private getSortedIndex(entry: Entry<T>, oldValue: T) {
|
||||||
const {output} = this;
|
const {_output: output} = this;
|
||||||
if (output[entry.approxIndex] === entry) {
|
if (output[entry.approxIndex] === entry) {
|
||||||
// yay!
|
// yay!
|
||||||
return entry.approxIndex;
|
return entry.approxIndex;
|
||||||
@@ -712,54 +841,12 @@ export class DataSource<
|
|||||||
private insertSorted(entry: Entry<T>) {
|
private insertSorted(entry: Entry<T>) {
|
||||||
// apply sorting
|
// apply sorting
|
||||||
const insertionIndex = sortedLastIndexBy(
|
const insertionIndex = sortedLastIndexBy(
|
||||||
this.output,
|
this._output,
|
||||||
entry,
|
entry,
|
||||||
this.sortHelper,
|
this.sortHelper,
|
||||||
);
|
);
|
||||||
entry.approxIndex = insertionIndex;
|
entry.approxIndex = insertionIndex;
|
||||||
this.output.splice(insertionIndex, 0, entry);
|
this._output.splice(insertionIndex, 0, entry);
|
||||||
this.notifyItemShift(insertionIndex, 1);
|
this.notifyItemShift(insertionIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateDataSourceOptions<T, K extends keyof T> = {
|
|
||||||
/**
|
|
||||||
* If a key is set, the given field of the records is assumed to be unique,
|
|
||||||
* and it's value can be used to perform lookups and upserts.
|
|
||||||
*/
|
|
||||||
key?: K;
|
|
||||||
/**
|
|
||||||
* The maximum amount of records that this DataSource will store.
|
|
||||||
* If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
/**
|
|
||||||
* Should this state persist when exporting a plugin?
|
|
||||||
* If set, the dataSource will be saved / loaded under the key provided
|
|
||||||
*/
|
|
||||||
persist?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createDataSource<T, KEY extends keyof T = any>(
|
|
||||||
initialSet: T[],
|
|
||||||
options: CreateDataSourceOptions<T, KEY>,
|
|
||||||
): DataSource<T, KEY, ExtractKeyType<T, KEY>>;
|
|
||||||
export function createDataSource<T>(
|
|
||||||
initialSet?: T[],
|
|
||||||
): DataSource<T, never, never>;
|
|
||||||
export function createDataSource<T, KEY extends keyof T>(
|
|
||||||
initialSet: T[] = [],
|
|
||||||
options?: CreateDataSourceOptions<T, KEY>,
|
|
||||||
): DataSource<T, any, any> {
|
|
||||||
const ds = new DataSource<T, KEY>(options?.key);
|
|
||||||
if (options?.limit !== undefined) {
|
|
||||||
ds.limit = options.limit;
|
|
||||||
}
|
|
||||||
registerStorageAtom(options?.persist, ds);
|
|
||||||
initialSet.forEach((value) => ds.append(value));
|
|
||||||
return ds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrap<T>(entry: Entry<T>): T {
|
|
||||||
return entry.value;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,43 +34,52 @@ function unwrap<T>(array: readonly {value: T}[]): readonly T[] {
|
|||||||
return array.map((entry) => entry.value);
|
return array.map((entry) => entry.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rawOutput<T>(ds: DataSource<T>): readonly T[] {
|
||||||
|
// @ts-ignore
|
||||||
|
const output = ds.view._output;
|
||||||
|
return unwrap(output);
|
||||||
|
}
|
||||||
|
|
||||||
test('can create a datasource', () => {
|
test('can create a datasource', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie]);
|
const ds = createDataSource<Todo>([eatCookie]);
|
||||||
expect(ds.records).toEqual([eatCookie]);
|
expect(ds.records()).toEqual([eatCookie]);
|
||||||
|
|
||||||
ds.append(drinkCoffee);
|
ds.append(drinkCoffee);
|
||||||
expect(ds.records).toEqual([eatCookie, drinkCoffee]);
|
expect(ds.records()).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
|
||||||
expect(() => ds.recordsById).toThrow(/Records cannot be looked up by key/);
|
// @ts-ignore
|
||||||
|
expect(() => ds.getById('stuff')).toThrow(
|
||||||
|
/Records cannot be looked up by key/,
|
||||||
|
);
|
||||||
|
|
||||||
ds.update(1, submitBug);
|
ds.update(1, submitBug);
|
||||||
expect(ds.records[1]).toBe(submitBug);
|
expect(ds.records()[1]).toBe(submitBug);
|
||||||
|
|
||||||
ds.remove(0);
|
ds.delete(0);
|
||||||
expect(ds.records[0]).toBe(submitBug);
|
expect(ds.records()[0]).toBe(submitBug);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can create a keyed datasource', () => {
|
test('can create a keyed datasource', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
|
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
|
||||||
expect(ds.records).toEqual([eatCookie]);
|
expect(ds.records()).toEqual([eatCookie]);
|
||||||
|
|
||||||
ds.append(drinkCoffee);
|
ds.append(drinkCoffee);
|
||||||
expect(ds.records).toEqual([eatCookie, drinkCoffee]);
|
expect(ds.records()).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
|
||||||
expect(ds.recordsById.get('bug')).toBe(undefined);
|
expect(ds.getById('bug')).toBe(undefined);
|
||||||
expect(ds.recordsById.get('cookie')).toBe(eatCookie);
|
expect(ds.getById('cookie')).toBe(eatCookie);
|
||||||
expect(ds.recordsById.get('coffee')).toBe(drinkCoffee);
|
expect(ds.getById('coffee')).toBe(drinkCoffee);
|
||||||
expect(ds.indexOfKey('bug')).toBe(-1);
|
expect(ds.getIndexOfKey('bug')).toBe(-1);
|
||||||
expect(ds.indexOfKey('cookie')).toBe(0);
|
expect(ds.getIndexOfKey('cookie')).toBe(0);
|
||||||
expect(ds.indexOfKey('coffee')).toBe(1);
|
expect(ds.getIndexOfKey('coffee')).toBe(1);
|
||||||
|
|
||||||
ds.update(1, submitBug);
|
ds.update(1, submitBug);
|
||||||
expect(ds.records[1]).toBe(submitBug);
|
expect(ds.records()[1]).toBe(submitBug);
|
||||||
expect(ds.recordsById.get('coffee')).toBe(undefined);
|
expect(ds.getById('coffee')).toBe(undefined);
|
||||||
expect(ds.recordsById.get('bug')).toBe(submitBug);
|
expect(ds.getById('bug')).toBe(submitBug);
|
||||||
expect(ds.indexOfKey('bug')).toBe(1);
|
expect(ds.getIndexOfKey('bug')).toBe(1);
|
||||||
expect(ds.indexOfKey('cookie')).toBe(0);
|
expect(ds.getIndexOfKey('cookie')).toBe(0);
|
||||||
expect(ds.indexOfKey('coffee')).toBe(-1);
|
expect(ds.getIndexOfKey('coffee')).toBe(-1);
|
||||||
|
|
||||||
// upsert existing
|
// upsert existing
|
||||||
const newBug = {
|
const newBug = {
|
||||||
@@ -79,8 +88,8 @@ test('can create a keyed datasource', () => {
|
|||||||
done: true,
|
done: true,
|
||||||
};
|
};
|
||||||
ds.upsert(newBug);
|
ds.upsert(newBug);
|
||||||
expect(ds.records[1]).toBe(newBug);
|
expect(ds.records()[1]).toBe(newBug);
|
||||||
expect(ds.recordsById.get('bug')).toBe(newBug);
|
expect(ds.getById('bug')).toBe(newBug);
|
||||||
|
|
||||||
// upsert new
|
// upsert new
|
||||||
const trash = {
|
const trash = {
|
||||||
@@ -88,16 +97,16 @@ test('can create a keyed datasource', () => {
|
|||||||
title: 'take trash out',
|
title: 'take trash out',
|
||||||
};
|
};
|
||||||
ds.upsert(trash);
|
ds.upsert(trash);
|
||||||
expect(ds.records[2]).toBe(trash);
|
expect(ds.records()[2]).toBe(trash);
|
||||||
expect(ds.recordsById.get('trash')).toBe(trash);
|
expect(ds.getById('trash')).toBe(trash);
|
||||||
|
|
||||||
// delete by key
|
// delete by key
|
||||||
expect(ds.records).toEqual([eatCookie, newBug, trash]);
|
expect(ds.records()).toEqual([eatCookie, newBug, trash]);
|
||||||
expect(ds.removeByKey('bug')).toBe(true);
|
expect(ds.deleteByKey('bug')).toBe(true);
|
||||||
expect(ds.records).toEqual([eatCookie, trash]);
|
expect(ds.records()).toEqual([eatCookie, trash]);
|
||||||
expect(ds.indexOfKey('bug')).toBe(-1);
|
expect(ds.getIndexOfKey('bug')).toBe(-1);
|
||||||
expect(ds.indexOfKey('cookie')).toBe(0);
|
expect(ds.getIndexOfKey('cookie')).toBe(0);
|
||||||
expect(ds.indexOfKey('trash')).toBe(1);
|
expect(ds.getIndexOfKey('trash')).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throws on invalid keys', () => {
|
test('throws on invalid keys', () => {
|
||||||
@@ -110,32 +119,41 @@ test('throws on invalid keys', () => {
|
|||||||
}).toThrow(`Duplicate key: 'cookie'`);
|
}).toThrow(`Duplicate key: 'cookie'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('throws on update causing duplicate key', () => {
|
||||||
|
const ds = createDataSource<Todo>([eatCookie, submitBug], {key: 'id'});
|
||||||
|
expect(() => {
|
||||||
|
ds.update(0, {id: 'bug', title: 'oops'});
|
||||||
|
}).toThrow(
|
||||||
|
`Trying to insert duplicate key 'bug', which already exist in the collection`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('removing invalid keys', () => {
|
test('removing invalid keys', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
|
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
|
||||||
expect(ds.removeByKey('trash')).toBe(false);
|
expect(ds.deleteByKey('trash')).toBe(false);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
ds.remove(1);
|
ds.delete(1);
|
||||||
}).toThrowError('Out of bounds');
|
}).toThrowError('Out of bounds');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sorting works', () => {
|
test('sorting works', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
||||||
ds.setSortBy((todo) => todo.title);
|
ds.view.setSortBy((todo) => todo.title);
|
||||||
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
|
expect(rawOutput(ds)).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
ds.setSortBy(undefined);
|
ds.view.setSortBy(undefined);
|
||||||
ds.setSortBy(undefined);
|
ds.view.setSortBy(undefined);
|
||||||
expect(unwrap(ds.output)).toEqual([eatCookie, drinkCoffee]);
|
expect(rawOutput(ds)).toEqual([eatCookie, drinkCoffee]);
|
||||||
ds.setSortBy((todo) => todo.title);
|
ds.view.setSortBy((todo) => todo.title);
|
||||||
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
|
expect(rawOutput(ds)).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
const aleph = {
|
const aleph = {
|
||||||
id: 'd',
|
id: 'd',
|
||||||
title: 'aleph',
|
title: 'aleph',
|
||||||
};
|
};
|
||||||
ds.append(aleph);
|
ds.append(aleph);
|
||||||
expect(ds.records).toEqual([eatCookie, drinkCoffee, aleph]);
|
expect(ds.records()).toEqual([eatCookie, drinkCoffee, aleph]);
|
||||||
expect(unwrap(ds.output)).toEqual([aleph, drinkCoffee, eatCookie]);
|
expect(rawOutput(ds)).toEqual([aleph, drinkCoffee, eatCookie]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sorting preserves insertion order with equal keys', () => {
|
test('sorting preserves insertion order with equal keys', () => {
|
||||||
@@ -151,15 +169,15 @@ test('sorting preserves insertion order with equal keys', () => {
|
|||||||
const c = {$: 'c', name: 'c'};
|
const c = {$: 'c', name: 'c'};
|
||||||
|
|
||||||
const ds = createDataSource<N>([]);
|
const ds = createDataSource<N>([]);
|
||||||
ds.setSortBy('$');
|
ds.view.setSortBy('$');
|
||||||
ds.append(b1);
|
ds.append(b1);
|
||||||
ds.append(c);
|
ds.append(c);
|
||||||
ds.append(b2);
|
ds.append(b2);
|
||||||
ds.append(a);
|
ds.append(a);
|
||||||
ds.append(b3);
|
ds.append(b3);
|
||||||
|
|
||||||
expect(ds.records).toEqual([b1, c, b2, a, b3]);
|
expect(ds.records()).toEqual([b1, c, b2, a, b3]);
|
||||||
expect(unwrap(ds.output)).toEqual([a, b1, b2, b3, c]);
|
expect(rawOutput(ds)).toEqual([a, b1, b2, b3, c]);
|
||||||
|
|
||||||
// if we append a new item with existig item, it should end up in the end
|
// if we append a new item with existig item, it should end up in the end
|
||||||
const b4 = {
|
const b4 = {
|
||||||
@@ -167,8 +185,8 @@ test('sorting preserves insertion order with equal keys', () => {
|
|||||||
name: 'b4',
|
name: 'b4',
|
||||||
};
|
};
|
||||||
ds.append(b4);
|
ds.append(b4);
|
||||||
expect(ds.records).toEqual([b1, c, b2, a, b3, b4]);
|
expect(ds.records()).toEqual([b1, c, b2, a, b3, b4]);
|
||||||
expect(unwrap(ds.output)).toEqual([a, b1, b2, b3, b4, c]);
|
expect(rawOutput(ds)).toEqual([a, b1, b2, b3, b4, c]);
|
||||||
|
|
||||||
// if we replace the middle item, it should end up in the middle
|
// if we replace the middle item, it should end up in the middle
|
||||||
const b2r = {
|
const b2r = {
|
||||||
@@ -176,8 +194,8 @@ test('sorting preserves insertion order with equal keys', () => {
|
|||||||
name: 'b2replacement',
|
name: 'b2replacement',
|
||||||
};
|
};
|
||||||
ds.update(2, b2r);
|
ds.update(2, b2r);
|
||||||
expect(ds.records).toEqual([b1, c, b2r, a, b3, b4]);
|
expect(ds.records()).toEqual([b1, c, b2r, a, b3, b4]);
|
||||||
expect(unwrap(ds.output)).toEqual([a, b1, b2r, b3, b4, c]);
|
expect(rawOutput(ds)).toEqual([a, b1, b2r, b3, b4, c]);
|
||||||
|
|
||||||
// if we replace something with a different sort value, it should be sorted properly, and the old should disappear
|
// if we replace something with a different sort value, it should be sorted properly, and the old should disappear
|
||||||
const b3r = {
|
const b3r = {
|
||||||
@@ -185,29 +203,29 @@ test('sorting preserves insertion order with equal keys', () => {
|
|||||||
name: 'b3replacement',
|
name: 'b3replacement',
|
||||||
};
|
};
|
||||||
ds.update(4, b3r);
|
ds.update(4, b3r);
|
||||||
expect(ds.records).toEqual([b1, c, b2r, a, b3r, b4]);
|
expect(ds.records()).toEqual([b1, c, b2r, a, b3r, b4]);
|
||||||
expect(unwrap(ds.output)).toEqual([a, b3r, b1, b2r, b4, c]);
|
expect(rawOutput(ds)).toEqual([a, b3r, b1, b2r, b4, c]);
|
||||||
|
|
||||||
ds.remove(3);
|
ds.delete(3);
|
||||||
expect(ds.records).toEqual([b1, c, b2r, b3r, b4]);
|
expect(ds.records()).toEqual([b1, c, b2r, b3r, b4]);
|
||||||
expect(unwrap(ds.output)).toEqual([b3r, b1, b2r, b4, c]);
|
expect(rawOutput(ds)).toEqual([b3r, b1, b2r, b4, c]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filter + sort', () => {
|
test('filter + sort', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug]);
|
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug]);
|
||||||
|
|
||||||
ds.setFilter((t) => t.title.indexOf('c') === -1);
|
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
|
||||||
ds.setSortBy('title');
|
ds.view.setSortBy('title');
|
||||||
|
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
|
|
||||||
// append with and without filter
|
// append with and without filter
|
||||||
const a = {id: 'a', title: 'does have that letter: c'};
|
const a = {id: 'a', title: 'does have that letter: c'};
|
||||||
const b = {id: 'b', title: 'doesnt have that letter'};
|
const b = {id: 'b', title: 'doesnt have that letter'};
|
||||||
ds.append(a);
|
ds.append(a);
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
ds.append(b);
|
ds.append(b);
|
||||||
expect(unwrap(ds.output)).toEqual([b, submitBug]);
|
expect(rawOutput(ds)).toEqual([b, submitBug]);
|
||||||
|
|
||||||
// filter in
|
// filter in
|
||||||
const newCookie = {
|
const newCookie = {
|
||||||
@@ -215,7 +233,7 @@ test('filter + sort', () => {
|
|||||||
title: 'eat a ookie',
|
title: 'eat a ookie',
|
||||||
};
|
};
|
||||||
ds.update(0, newCookie);
|
ds.update(0, newCookie);
|
||||||
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([b, newCookie, submitBug]);
|
||||||
|
|
||||||
// update -> filter in
|
// update -> filter in
|
||||||
const newCoffee = {
|
const newCoffee = {
|
||||||
@@ -223,35 +241,25 @@ test('filter + sort', () => {
|
|||||||
title: 'better drink tea',
|
title: 'better drink tea',
|
||||||
};
|
};
|
||||||
ds.append(newCoffee);
|
ds.append(newCoffee);
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie, submitBug]);
|
||||||
|
|
||||||
// update -> filter out
|
// update -> filter out
|
||||||
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie]);
|
||||||
|
|
||||||
ds.update(2, submitBug);
|
ds.update(2, submitBug);
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie, submitBug]);
|
||||||
|
|
||||||
ds.remove(3); // a
|
ds.delete(3); // a
|
||||||
ds.remove(3); // b
|
ds.delete(3); // b
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, newCookie, submitBug]);
|
||||||
|
|
||||||
ds.setFilter(undefined);
|
ds.view.setFilter(undefined);
|
||||||
expect(unwrap(ds.output)).toEqual([
|
expect(rawOutput(ds)).toEqual([newCoffee, drinkCoffee, newCookie, submitBug]);
|
||||||
newCoffee,
|
|
||||||
drinkCoffee,
|
|
||||||
newCookie,
|
|
||||||
submitBug,
|
|
||||||
]);
|
|
||||||
|
|
||||||
ds.setSortBy(undefined);
|
ds.view.setSortBy(undefined);
|
||||||
// key insertion order
|
// key insertion order
|
||||||
expect(unwrap(ds.output)).toEqual([
|
expect(rawOutput(ds)).toEqual([newCookie, drinkCoffee, submitBug, newCoffee]);
|
||||||
newCookie,
|
|
||||||
drinkCoffee,
|
|
||||||
submitBug,
|
|
||||||
newCoffee,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filter + sort + index', () => {
|
test('filter + sort + index', () => {
|
||||||
@@ -259,18 +267,18 @@ test('filter + sort + index', () => {
|
|||||||
key: 'id',
|
key: 'id',
|
||||||
});
|
});
|
||||||
|
|
||||||
ds.setFilter((t) => t.title.indexOf('c') === -1);
|
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
|
||||||
ds.setSortBy('title');
|
ds.view.setSortBy('title');
|
||||||
|
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
|
|
||||||
// append with and without filter
|
// append with and without filter
|
||||||
const a = {id: 'a', title: 'does have that letter: c'};
|
const a = {id: 'a', title: 'does have that letter: c'};
|
||||||
const b = {id: 'b', title: 'doesnt have that letter'};
|
const b = {id: 'b', title: 'doesnt have that letter'};
|
||||||
ds.append(a);
|
ds.append(a);
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
ds.append(b);
|
ds.append(b);
|
||||||
expect(unwrap(ds.output)).toEqual([b, submitBug]);
|
expect(rawOutput(ds)).toEqual([b, submitBug]);
|
||||||
|
|
||||||
// filter in
|
// filter in
|
||||||
const newCookie = {
|
const newCookie = {
|
||||||
@@ -278,7 +286,7 @@ test('filter + sort + index', () => {
|
|||||||
title: 'eat a ookie',
|
title: 'eat a ookie',
|
||||||
};
|
};
|
||||||
ds.update(0, newCookie);
|
ds.update(0, newCookie);
|
||||||
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([b, newCookie, submitBug]);
|
||||||
|
|
||||||
// update -> filter in
|
// update -> filter in
|
||||||
const newCoffee = {
|
const newCoffee = {
|
||||||
@@ -286,24 +294,24 @@ test('filter + sort + index', () => {
|
|||||||
title: 'better drink tea',
|
title: 'better drink tea',
|
||||||
};
|
};
|
||||||
ds.upsert(newCoffee);
|
ds.upsert(newCoffee);
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie, submitBug]);
|
||||||
|
|
||||||
// update -> filter out
|
// update -> filter out
|
||||||
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie]);
|
||||||
|
|
||||||
ds.update(2, submitBug);
|
ds.update(2, submitBug);
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie, submitBug]);
|
||||||
|
|
||||||
ds.setFilter(undefined);
|
ds.view.setFilter(undefined);
|
||||||
expect(unwrap(ds.output)).toEqual([newCoffee, a, b, newCookie, submitBug]);
|
expect(rawOutput(ds)).toEqual([newCoffee, a, b, newCookie, submitBug]);
|
||||||
|
|
||||||
ds.setSortBy(undefined);
|
ds.view.setSortBy(undefined);
|
||||||
// key insertion order
|
// key insertion order
|
||||||
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
|
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, submitBug, a, b]);
|
||||||
// verify getOutput
|
// verify getOutput
|
||||||
expect(unwrap(ds.output.slice(1, 3))).toEqual([newCoffee, submitBug]);
|
expect(rawOutput(ds).slice(1, 3)).toEqual([newCoffee, submitBug]);
|
||||||
expect(ds.getOutput(1, 3)).toEqual([newCoffee, submitBug]);
|
expect(ds.view.output(1, 3)).toEqual([newCoffee, submitBug]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filter', () => {
|
test('filter', () => {
|
||||||
@@ -311,16 +319,16 @@ test('filter', () => {
|
|||||||
key: 'id',
|
key: 'id',
|
||||||
});
|
});
|
||||||
|
|
||||||
ds.setFilter((t) => t.title.indexOf('c') === -1);
|
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
|
|
||||||
// append with and without filter
|
// append with and without filter
|
||||||
const a = {id: 'a', title: 'does have that letter: c'};
|
const a = {id: 'a', title: 'does have that letter: c'};
|
||||||
const b = {id: 'b', title: 'doesnt have that letter'};
|
const b = {id: 'b', title: 'doesnt have that letter'};
|
||||||
ds.append(a);
|
ds.append(a);
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug]);
|
expect(rawOutput(ds)).toEqual([submitBug]);
|
||||||
ds.append(b);
|
ds.append(b);
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug, b]);
|
expect(rawOutput(ds)).toEqual([submitBug, b]);
|
||||||
|
|
||||||
// filter in
|
// filter in
|
||||||
const newCookie = {
|
const newCookie = {
|
||||||
@@ -328,7 +336,7 @@ test('filter', () => {
|
|||||||
title: 'eat a ookie',
|
title: 'eat a ookie',
|
||||||
};
|
};
|
||||||
ds.update(0, newCookie);
|
ds.update(0, newCookie);
|
||||||
expect(unwrap(ds.output)).toEqual([newCookie, submitBug, b]);
|
expect(rawOutput(ds)).toEqual([newCookie, submitBug, b]);
|
||||||
|
|
||||||
// update -> filter in
|
// update -> filter in
|
||||||
const newCoffee = {
|
const newCoffee = {
|
||||||
@@ -336,48 +344,48 @@ test('filter', () => {
|
|||||||
title: 'better drink tea',
|
title: 'better drink tea',
|
||||||
};
|
};
|
||||||
ds.upsert(newCoffee);
|
ds.upsert(newCoffee);
|
||||||
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, b]);
|
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, submitBug, b]);
|
||||||
|
|
||||||
// update -> filter out
|
// update -> filter out
|
||||||
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
ds.update(2, {id: 'bug', title: 'bug has c!'});
|
||||||
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, b]);
|
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, b]);
|
||||||
|
|
||||||
ds.update(2, submitBug);
|
ds.update(2, submitBug);
|
||||||
|
|
||||||
ds.setFilter(undefined);
|
ds.view.setFilter(undefined);
|
||||||
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
|
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, submitBug, a, b]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reverse without sorting', () => {
|
test('reverse without sorting', () => {
|
||||||
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
expect(ds.getOutput()).toEqual([eatCookie, drinkCoffee]);
|
expect(ds.view.output()).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
|
||||||
ds.toggleReversed();
|
ds.view.toggleReversed();
|
||||||
expect(ds.getOutput(1, 2)).toEqual([eatCookie]);
|
expect(ds.view.output(1, 2)).toEqual([eatCookie]);
|
||||||
expect(ds.getOutput(0, 1)).toEqual([drinkCoffee]);
|
expect(ds.view.output(0, 1)).toEqual([drinkCoffee]);
|
||||||
expect(ds.getOutput(0, 2)).toEqual([drinkCoffee, eatCookie]);
|
expect(ds.view.output(0, 2)).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
expect(ds.getOutput()).toEqual([drinkCoffee, eatCookie]);
|
expect(ds.view.output()).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
ds.append(submitBug);
|
ds.append(submitBug);
|
||||||
expect(ds.records).toEqual([eatCookie, drinkCoffee, submitBug]);
|
expect(ds.records()).toEqual([eatCookie, drinkCoffee, submitBug]);
|
||||||
expect(ds.getOutput()).toEqual([submitBug, drinkCoffee, eatCookie]);
|
expect(ds.view.output()).toEqual([submitBug, drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
const x = {id: 'x', title: 'x'};
|
const x = {id: 'x', title: 'x'};
|
||||||
ds.update(0, x);
|
ds.update(0, x);
|
||||||
expect(ds.records).toEqual([x, drinkCoffee, submitBug]);
|
expect(ds.records()).toEqual([x, drinkCoffee, submitBug]);
|
||||||
expect(ds.getOutput()).toEqual([submitBug, drinkCoffee, x]);
|
expect(ds.view.output()).toEqual([submitBug, drinkCoffee, x]);
|
||||||
const y = {id: 'y', title: 'y'};
|
const y = {id: 'y', title: 'y'};
|
||||||
const z = {id: 'z', title: 'z'};
|
const z = {id: 'z', title: 'z'};
|
||||||
ds.update(2, z);
|
ds.update(2, z);
|
||||||
ds.update(1, y);
|
ds.update(1, y);
|
||||||
|
|
||||||
expect(ds.records).toEqual([x, y, z]);
|
expect(ds.records()).toEqual([x, y, z]);
|
||||||
expect(ds.getOutput()).toEqual([z, y, x]);
|
expect(ds.view.output()).toEqual([z, y, x]);
|
||||||
|
|
||||||
ds.setReversed(false);
|
ds.view.setReversed(false);
|
||||||
expect(ds.getOutput()).toEqual([x, y, z]);
|
expect(ds.view.output()).toEqual([x, y, z]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reverse with sorting', () => {
|
test('reverse with sorting', () => {
|
||||||
@@ -393,23 +401,23 @@ test('reverse with sorting', () => {
|
|||||||
const c = {$: 'c', name: 'c'};
|
const c = {$: 'c', name: 'c'};
|
||||||
|
|
||||||
const ds = createDataSource<N>([]);
|
const ds = createDataSource<N>([]);
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.setReversed(true);
|
ds.view.setReversed(true);
|
||||||
ds.append(b1);
|
ds.append(b1);
|
||||||
ds.append(c);
|
ds.append(c);
|
||||||
expect(ds.getOutput()).toEqual([c, b1]);
|
expect(ds.view.output()).toEqual([c, b1]);
|
||||||
|
|
||||||
ds.setSortBy('$');
|
ds.view.setSortBy('$');
|
||||||
expect(ds.getOutput()).toEqual([c, b1]);
|
expect(ds.view.output()).toEqual([c, b1]);
|
||||||
|
|
||||||
ds.append(b2);
|
ds.append(b2);
|
||||||
expect(ds.getOutput()).toEqual([c, b2, b1]);
|
expect(ds.view.output()).toEqual([c, b2, b1]);
|
||||||
|
|
||||||
ds.append(a);
|
ds.append(a);
|
||||||
expect(ds.getOutput()).toEqual([c, b2, b1, a]);
|
expect(ds.view.output()).toEqual([c, b2, b1, a]);
|
||||||
|
|
||||||
ds.append(b3);
|
ds.append(b3);
|
||||||
expect(ds.getOutput()).toEqual([c, b3, b2, b1, a]);
|
expect(ds.view.output()).toEqual([c, b3, b2, b1, a]);
|
||||||
|
|
||||||
// if we append a new item with existig item, it should end up in the end
|
// if we append a new item with existig item, it should end up in the end
|
||||||
const b4 = {
|
const b4 = {
|
||||||
@@ -417,7 +425,7 @@ test('reverse with sorting', () => {
|
|||||||
name: 'b4',
|
name: 'b4',
|
||||||
};
|
};
|
||||||
ds.append(b4);
|
ds.append(b4);
|
||||||
expect(ds.getOutput()).toEqual([c, b4, b3, b2, b1, a]);
|
expect(ds.view.output()).toEqual([c, b4, b3, b2, b1, a]);
|
||||||
|
|
||||||
// if we replace the middle item, it should end up in the middle
|
// if we replace the middle item, it should end up in the middle
|
||||||
const b2r = {
|
const b2r = {
|
||||||
@@ -425,7 +433,7 @@ test('reverse with sorting', () => {
|
|||||||
name: 'b2replacement',
|
name: 'b2replacement',
|
||||||
};
|
};
|
||||||
ds.update(2, b2r);
|
ds.update(2, b2r);
|
||||||
expect(ds.getOutput()).toEqual([c, b4, b3, b2r, b1, a]);
|
expect(ds.view.output()).toEqual([c, b4, b3, b2r, b1, a]);
|
||||||
|
|
||||||
// if we replace something with a different sort value, it should be sorted properly, and the old should disappear
|
// if we replace something with a different sort value, it should be sorted properly, and the old should disappear
|
||||||
const b3r = {
|
const b3r = {
|
||||||
@@ -433,45 +441,45 @@ test('reverse with sorting', () => {
|
|||||||
name: 'b3replacement',
|
name: 'b3replacement',
|
||||||
};
|
};
|
||||||
ds.update(4, b3r);
|
ds.update(4, b3r);
|
||||||
expect(ds.getOutput()).toEqual([c, b4, b2r, b1, b3r, a]);
|
expect(ds.view.output()).toEqual([c, b4, b2r, b1, b3r, a]);
|
||||||
|
|
||||||
ds.remove(4);
|
ds.delete(4);
|
||||||
expect(ds.getOutput()).toEqual([c, b4, b2r, b1, a]);
|
expect(ds.view.output()).toEqual([c, b4, b2r, b1, a]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reset', () => {
|
test('reset', () => {
|
||||||
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
|
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
|
||||||
key: 'id',
|
key: 'id',
|
||||||
});
|
});
|
||||||
ds.setSortBy('title');
|
ds.view.setSortBy('title');
|
||||||
ds.setFilter((v) => v.id !== 'cookie');
|
ds.view.setFilter((v) => v.id !== 'cookie');
|
||||||
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
|
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
|
||||||
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
||||||
|
|
||||||
ds.reset();
|
ds.view.reset();
|
||||||
expect(unwrap(ds.output)).toEqual([submitBug, drinkCoffee, eatCookie]);
|
expect(rawOutput(ds)).toEqual([submitBug, drinkCoffee, eatCookie]);
|
||||||
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clear', () => {
|
test('clear', () => {
|
||||||
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
|
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
|
||||||
key: 'id',
|
key: 'id',
|
||||||
});
|
});
|
||||||
ds.setSortBy('title');
|
ds.view.setSortBy('title');
|
||||||
ds.setFilter((v) => v.id !== 'cookie');
|
ds.view.setFilter((v) => v.id !== 'cookie');
|
||||||
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
|
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
|
||||||
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
|
||||||
|
|
||||||
ds.clear();
|
ds.clear();
|
||||||
expect(unwrap(ds.output)).toEqual([]);
|
expect(rawOutput(ds)).toEqual([]);
|
||||||
expect([...ds.recordsById.keys()]).toEqual([]);
|
expect([...ds.keys()]).toEqual([]);
|
||||||
|
|
||||||
ds.append(eatCookie);
|
ds.append(eatCookie);
|
||||||
ds.append(drinkCoffee);
|
ds.append(drinkCoffee);
|
||||||
ds.append(submitBug);
|
ds.append(submitBug);
|
||||||
expect([...ds.recordsById.keys()]).toEqual(['cookie', 'coffee', 'bug']);
|
expect([...ds.keys()]).toEqual(['cookie', 'coffee', 'bug']);
|
||||||
// resets in the same ordering as view preferences were preserved
|
// resets in the same ordering as view preferences were preserved
|
||||||
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
|
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
|
||||||
});
|
});
|
||||||
|
|
||||||
function testEvents<T>(
|
function testEvents<T>(
|
||||||
@@ -481,9 +489,9 @@ function testEvents<T>(
|
|||||||
): any[] {
|
): any[] {
|
||||||
const ds = createDataSource<T>(initial, {key});
|
const ds = createDataSource<T>(initial, {key});
|
||||||
const events: any[] = [];
|
const events: any[] = [];
|
||||||
ds.setOutputChangeListener((e) => events.push(e));
|
ds.view.setListener((e) => events.push(e));
|
||||||
op(ds);
|
op(ds);
|
||||||
ds.setOutputChangeListener(undefined);
|
ds.view.setListener(undefined);
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +515,7 @@ test('it emits the right events - zero window', () => {
|
|||||||
test('it emits the right events - small window', () => {
|
test('it emits the right events - small window', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['a', 'b'], (ds) => {
|
testEvents(['a', 'b'], (ds) => {
|
||||||
ds.setWindow(0, 3);
|
ds.view.setWindow(0, 3);
|
||||||
ds.append('c');
|
ds.append('c');
|
||||||
ds.update(1, 'x');
|
ds.update(1, 'x');
|
||||||
}),
|
}),
|
||||||
@@ -520,13 +528,13 @@ test('it emits the right events - small window', () => {
|
|||||||
test('it emits the right events - view change', () => {
|
test('it emits the right events - view change', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['a', 'b'], (ds) => {
|
testEvents(['a', 'b'], (ds) => {
|
||||||
ds.setWindow(1, 2);
|
ds.view.setWindow(1, 2);
|
||||||
ds.setSortBy((x) => x);
|
ds.view.setSortBy((x) => x);
|
||||||
// a, [b]
|
// a, [b]
|
||||||
ds.update(0, 'x');
|
ds.update(0, 'x');
|
||||||
// b, [x]
|
// b, [x]
|
||||||
expect(ds.getItem(0)).toEqual('b');
|
expect(ds.view.get(0)).toEqual('b');
|
||||||
expect(ds.getItem(1)).toEqual('x');
|
expect(ds.view.get(1)).toEqual('x');
|
||||||
ds.append('y');
|
ds.append('y');
|
||||||
// b, [x], y
|
// b, [x], y
|
||||||
ds.append('c');
|
ds.append('c');
|
||||||
@@ -544,14 +552,14 @@ test('it emits the right events - view change', () => {
|
|||||||
test('it emits the right events - reversed view change', () => {
|
test('it emits the right events - reversed view change', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['a', 'b'], (ds) => {
|
testEvents(['a', 'b'], (ds) => {
|
||||||
ds.setWindow(1, 2);
|
ds.view.setWindow(1, 2);
|
||||||
ds.setSortBy((x) => x);
|
ds.view.setSortBy((x) => x);
|
||||||
ds.setReversed(true);
|
ds.view.setReversed(true);
|
||||||
// b, [a]
|
// b, [a]
|
||||||
ds.update(0, 'x');
|
ds.update(0, 'x');
|
||||||
// x, [b]
|
// x, [b]
|
||||||
expect(ds.getItem(0)).toEqual('x');
|
expect(ds.view.get(0)).toEqual('x');
|
||||||
expect(ds.getItem(1)).toEqual('b');
|
expect(ds.view.get(1)).toEqual('b');
|
||||||
ds.append('y');
|
ds.append('y');
|
||||||
// y, [x], b
|
// y, [x], b
|
||||||
ds.append('c');
|
ds.append('c');
|
||||||
@@ -573,15 +581,15 @@ test('it emits the right events - reversed view change', () => {
|
|||||||
test('it emits the right events - reversed view change with filter', () => {
|
test('it emits the right events - reversed view change with filter', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['a', 'b'], (ds) => {
|
testEvents(['a', 'b'], (ds) => {
|
||||||
ds.setWindow(0, 2);
|
ds.view.setWindow(0, 2);
|
||||||
ds.setSortBy((x) => x);
|
ds.view.setSortBy((x) => x);
|
||||||
ds.setReversed(true);
|
ds.view.setReversed(true);
|
||||||
ds.setFilter((x) => ['a', 'b'].includes(x));
|
ds.view.setFilter((x) => ['a', 'b'].includes(x));
|
||||||
// [b, a]
|
// [b, a]
|
||||||
ds.update(0, 'x'); // x b
|
ds.update(0, 'x'); // x b
|
||||||
// [b, ]
|
// [b, ]
|
||||||
expect(ds.getItem(0)).toEqual('b');
|
expect(ds.view.get(0)).toEqual('b');
|
||||||
expect(ds.output.length).toBe(1);
|
expect(rawOutput(ds).length).toBe(1);
|
||||||
ds.append('y'); // x b y
|
ds.append('y'); // x b y
|
||||||
// [b, ]
|
// [b, ]
|
||||||
ds.append('c'); // x b y c
|
ds.append('c'); // x b y c
|
||||||
@@ -590,9 +598,9 @@ test('it emits the right events - reversed view change with filter', () => {
|
|||||||
// [b, a]
|
// [b, a]
|
||||||
ds.append('a'); // x b y c a a
|
ds.append('a'); // x b y c a a
|
||||||
// [b, a, a] // N.b. the new a is in the *middle*
|
// [b, a, a] // N.b. the new a is in the *middle*
|
||||||
ds.remove(2); // x b c a a
|
ds.delete(2); // x b c a a
|
||||||
// no effect
|
// no effect
|
||||||
ds.remove(4); // this removes the second a in input, so the first a in the outpat!
|
ds.delete(4); // this removes the second a in input, so the first a in the outpat!
|
||||||
// [b, a]
|
// [b, a]
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
@@ -612,16 +620,16 @@ test('basic remove', () => {
|
|||||||
testEvents(
|
testEvents(
|
||||||
[drinkCoffee, eatCookie, submitBug],
|
[drinkCoffee, eatCookie, submitBug],
|
||||||
(ds) => {
|
(ds) => {
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.remove(0);
|
ds.delete(0);
|
||||||
expect(ds.getOutput()).toEqual([eatCookie, submitBug]);
|
expect(ds.view.output()).toEqual([eatCookie, submitBug]);
|
||||||
expect(ds.recordsById.get('bug')).toBe(submitBug);
|
expect(ds.getById('bug')).toBe(submitBug);
|
||||||
expect(ds.recordsById.get('coffee')).toBeUndefined();
|
expect(ds.getById('coffee')).toBeUndefined();
|
||||||
expect(ds.recordsById.get('cookie')).toBe(eatCookie);
|
expect(ds.getById('cookie')).toBe(eatCookie);
|
||||||
ds.upsert(completedBug);
|
ds.upsert(completedBug);
|
||||||
ds.removeByKey('cookie');
|
ds.deleteByKey('cookie');
|
||||||
expect(ds.getOutput()).toEqual([completedBug]);
|
expect(ds.view.output()).toEqual([completedBug]);
|
||||||
expect(ds.recordsById.get('bug')).toBe(completedBug);
|
expect(ds.getById('bug')).toBe(completedBug);
|
||||||
},
|
},
|
||||||
'id',
|
'id',
|
||||||
),
|
),
|
||||||
@@ -653,16 +661,16 @@ test('basic shift', () => {
|
|||||||
testEvents(
|
testEvents(
|
||||||
[drinkCoffee, eatCookie, submitBug],
|
[drinkCoffee, eatCookie, submitBug],
|
||||||
(ds) => {
|
(ds) => {
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.shift(2);
|
ds.shift(2);
|
||||||
expect(ds.getOutput()).toEqual([submitBug]);
|
expect(ds.view.output()).toEqual([submitBug]);
|
||||||
expect(ds.recordsById.get('bug')).toBe(submitBug);
|
expect(ds.getById('bug')).toBe(submitBug);
|
||||||
expect(ds.recordsById.get('coffee')).toBeUndefined();
|
expect(ds.getById('coffee')).toBeUndefined();
|
||||||
expect(ds.indexOfKey('bug')).toBe(0);
|
expect(ds.getIndexOfKey('bug')).toBe(0);
|
||||||
expect(ds.indexOfKey('coffee')).toBe(-1);
|
expect(ds.getIndexOfKey('coffee')).toBe(-1);
|
||||||
ds.upsert(completedBug);
|
ds.upsert(completedBug);
|
||||||
expect(ds.getOutput()).toEqual([completedBug]);
|
expect(ds.view.output()).toEqual([completedBug]);
|
||||||
expect(ds.recordsById.get('bug')).toBe(completedBug);
|
expect(ds.getById('bug')).toBe(completedBug);
|
||||||
},
|
},
|
||||||
'id',
|
'id',
|
||||||
),
|
),
|
||||||
@@ -684,11 +692,11 @@ test('basic shift', () => {
|
|||||||
test('sorted shift', () => {
|
test('sorted shift', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
|
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.setSortBy((v) => v);
|
ds.view.setSortBy((v) => v);
|
||||||
expect(ds.getOutput()).toEqual(['a', 'b', 'c', 'd', 'e']);
|
expect(ds.view.output()).toEqual(['a', 'b', 'c', 'd', 'e']);
|
||||||
ds.shift(4);
|
ds.shift(4);
|
||||||
expect(ds.getOutput()).toEqual(['d']);
|
expect(ds.view.output()).toEqual(['d']);
|
||||||
ds.shift(1); // optimizes to reset
|
ds.shift(1); // optimizes to reset
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
@@ -704,11 +712,11 @@ test('sorted shift', () => {
|
|||||||
test('filtered shift', () => {
|
test('filtered shift', () => {
|
||||||
expect(
|
expect(
|
||||||
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
|
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.setFilter((v) => v !== 'b' && v !== 'e');
|
ds.view.setFilter((v) => v !== 'b' && v !== 'e');
|
||||||
expect(ds.getOutput()).toEqual(['c', 'a', 'd']);
|
expect(ds.view.output()).toEqual(['c', 'a', 'd']);
|
||||||
ds.shift(4);
|
ds.shift(4);
|
||||||
expect(ds.getOutput()).toEqual(['d']);
|
expect(ds.view.output()).toEqual(['d']);
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{newCount: 3, type: 'reset'}, // filter
|
{newCount: 3, type: 'reset'}, // filter
|
||||||
@@ -724,16 +732,16 @@ test('remove after shift works correctly', () => {
|
|||||||
testEvents(
|
testEvents(
|
||||||
[eatCookie, drinkCoffee, submitBug, a, b],
|
[eatCookie, drinkCoffee, submitBug, a, b],
|
||||||
(ds) => {
|
(ds) => {
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
ds.shift(2);
|
ds.shift(2);
|
||||||
ds.removeByKey('b');
|
ds.deleteByKey('b');
|
||||||
ds.removeByKey('bug');
|
ds.deleteByKey('bug');
|
||||||
expect(ds.getOutput()).toEqual([a]);
|
expect(ds.view.output()).toEqual([a]);
|
||||||
expect(ds.indexOfKey('cookie')).toBe(-1);
|
expect(ds.getIndexOfKey('cookie')).toBe(-1);
|
||||||
expect(ds.indexOfKey('coffee')).toBe(-1);
|
expect(ds.getIndexOfKey('coffee')).toBe(-1);
|
||||||
expect(ds.indexOfKey('bug')).toBe(-1);
|
expect(ds.getIndexOfKey('bug')).toBe(-1);
|
||||||
expect(ds.indexOfKey('a')).toBe(0);
|
expect(ds.getIndexOfKey('a')).toBe(0);
|
||||||
expect(ds.indexOfKey('b')).toBe(-1);
|
expect(ds.getIndexOfKey('b')).toBe(-1);
|
||||||
},
|
},
|
||||||
'id',
|
'id',
|
||||||
),
|
),
|
||||||
@@ -764,7 +772,7 @@ test('remove after shift works correctly', () => {
|
|||||||
|
|
||||||
test('respects limit', () => {
|
test('respects limit', () => {
|
||||||
const grab = (): [length: number, first: number, last: number] => {
|
const grab = (): [length: number, first: number, last: number] => {
|
||||||
const output = ds.getOutput();
|
const output = ds.view.output();
|
||||||
return [output.length, output[0], output[output.length - 1]];
|
return [output.length, output[0], output[output.length - 1]];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -772,7 +780,7 @@ test('respects limit', () => {
|
|||||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||||
{limit: 20},
|
{limit: 20},
|
||||||
);
|
);
|
||||||
ds.setWindow(0, 100);
|
ds.view.setWindow(0, 100);
|
||||||
|
|
||||||
ds.append(19);
|
ds.append(19);
|
||||||
ds.append(20);
|
ds.append(20);
|
||||||
@@ -783,7 +791,7 @@ test('respects limit', () => {
|
|||||||
ds.append(22);
|
ds.append(22);
|
||||||
expect(grab()).toEqual([20, 3, 22]);
|
expect(grab()).toEqual([20, 3, 22]);
|
||||||
|
|
||||||
ds.remove(0);
|
ds.delete(0);
|
||||||
expect(grab()).toEqual([19, 4, 22]);
|
expect(grab()).toEqual([19, 4, 22]);
|
||||||
|
|
||||||
ds.append(23);
|
ds.append(23);
|
||||||
@@ -791,3 +799,51 @@ test('respects limit', () => {
|
|||||||
ds.append(24);
|
ds.append(24);
|
||||||
expect(grab()).toEqual([19, 6, 24]);
|
expect(grab()).toEqual([19, 6, 24]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('DataSource can iterate', () => {
|
||||||
|
const ds = createDataSource([eatCookie, drinkCoffee], {key: 'id'});
|
||||||
|
|
||||||
|
expect([...ds]).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
expect(Array.from(ds.keys())).toEqual(['cookie', 'coffee']);
|
||||||
|
expect(Array.from(ds.entries())).toEqual([
|
||||||
|
['cookie', eatCookie],
|
||||||
|
['coffee', drinkCoffee],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seen: Todo[] = [];
|
||||||
|
for (const todo of ds) {
|
||||||
|
seen.push(todo);
|
||||||
|
}
|
||||||
|
expect(seen).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
|
||||||
|
ds.append(submitBug);
|
||||||
|
expect([...ds]).toEqual([eatCookie, drinkCoffee, submitBug]);
|
||||||
|
|
||||||
|
ds.clear();
|
||||||
|
expect([...ds]).toEqual([]);
|
||||||
|
|
||||||
|
ds.append(submitBug);
|
||||||
|
expect([...ds]).toEqual([submitBug]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DataSource.view can iterate', () => {
|
||||||
|
const ds = createDataSource([eatCookie, drinkCoffee, submitBug, eatCookie]);
|
||||||
|
ds.view.setSortBy('id');
|
||||||
|
// bug coffee cookie cookie
|
||||||
|
ds.view.toggleReversed();
|
||||||
|
// cookie cookie coffee bug
|
||||||
|
ds.view.setWindow(1, 3);
|
||||||
|
// cookie coffee
|
||||||
|
|
||||||
|
expect(ds.view.output()).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
expect([...ds.view]).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
|
||||||
|
ds.view.reset();
|
||||||
|
// default window is empty!
|
||||||
|
expect([...ds.view]).toEqual([]);
|
||||||
|
ds.view.setWindow(0, 100);
|
||||||
|
expect([...ds.view]).toEqual([eatCookie, drinkCoffee, submitBug, eatCookie]);
|
||||||
|
|
||||||
|
ds.clear();
|
||||||
|
expect([...ds.view]).toEqual([]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type DataSourceish = DataSource<Todo> & FakeDataSource<Todo>;
|
|||||||
test.skip('run perf test', () => {
|
test.skip('run perf test', () => {
|
||||||
if (!global.gc) {
|
if (!global.gc) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Warning: garbage collector not available, skipping this test',
|
'Warning: garbage collector not available, skipping this test. Make sure to start the test suite using `yarn watch`',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,10 +73,10 @@ test.skip('run perf test', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(datasources).forEach(([name, ds]) => {
|
Object.entries(datasources).forEach(([name, ds]) => {
|
||||||
ds.setWindow(0, 1000000);
|
ds.view.setWindow(0, 1000000);
|
||||||
if (name.includes('sorted')) {
|
if (name.includes('sorted')) {
|
||||||
ds.setFilter(defaultFilter);
|
ds.view.setFilter(defaultFilter);
|
||||||
ds.setSortBy('title');
|
ds.view.setSortBy('title');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ test.skip('run perf test', () => {
|
|||||||
// to 'render' we need to know the end result (this mimics a lazy evaluation of filter / sort)
|
// to 'render' we need to know the end result (this mimics a lazy evaluation of filter / sort)
|
||||||
// note that this skews the test a bit in favor of fake data source,
|
// note that this skews the test a bit in favor of fake data source,
|
||||||
// as DataSource would *always* keep things sorted/ filtered, but doing that would explode the test for append / update :)
|
// as DataSource would *always* keep things sorted/ filtered, but doing that would explode the test for append / update :)
|
||||||
ds.buildOutput();
|
ds.view.buildOutput();
|
||||||
}
|
}
|
||||||
// global.gc?.(); // to cleanup our createdmess as part of the measurement
|
// global.gc?.(); // to cleanup our createdmess as part of the measurement
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
@@ -119,7 +119,7 @@ test.skip('run perf test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
measure('remove', (ds) => {
|
measure('remove', (ds) => {
|
||||||
ds.remove(99);
|
ds.delete(99);
|
||||||
});
|
});
|
||||||
|
|
||||||
measure('shift', (ds) => {
|
measure('shift', (ds) => {
|
||||||
@@ -127,11 +127,11 @@ test.skip('run perf test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
measure('change sorting', (ds) => {
|
measure('change sorting', (ds) => {
|
||||||
ds.setSortBy('id');
|
ds.view.setSortBy('id');
|
||||||
});
|
});
|
||||||
|
|
||||||
measure('change filter', (ds) => {
|
measure('change filter', (ds) => {
|
||||||
ds.setFilter((t) => t.title.includes('23')); // 23 does not occur in original text
|
ds.view.setFilter((t) => t.title.includes('23')); // 23 does not occur in original text
|
||||||
});
|
});
|
||||||
|
|
||||||
const sum: any = {};
|
const sum: any = {};
|
||||||
@@ -159,37 +159,39 @@ class FakeDataSource<T> {
|
|||||||
|
|
||||||
constructor(initial: T[]) {
|
constructor(initial: T[]) {
|
||||||
this.data = initial;
|
this.data = initial;
|
||||||
this.buildOutput();
|
this.view.buildOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
setWindow(_start: number, _end: number) {
|
view = {
|
||||||
// noop
|
setWindow: (_start: number, _end: number) => {
|
||||||
}
|
// noop
|
||||||
|
},
|
||||||
|
|
||||||
setFilter(filter: (t: T) => boolean) {
|
setFilter: (filter: (t: T) => boolean) => {
|
||||||
this.filterFn = filter;
|
this.filterFn = filter;
|
||||||
}
|
},
|
||||||
|
|
||||||
setSortBy(k: keyof T) {
|
setSortBy: (k: keyof T) => {
|
||||||
this.sortAttr = k;
|
this.sortAttr = k;
|
||||||
}
|
},
|
||||||
|
|
||||||
buildOutput() {
|
buildOutput: () => {
|
||||||
const filtered = this.filterFn
|
const filtered = this.filterFn
|
||||||
? this.data.filter(this.filterFn)
|
? this.data.filter(this.filterFn)
|
||||||
: this.data;
|
: this.data;
|
||||||
const sorted = this.sortAttr
|
const sorted = this.sortAttr
|
||||||
? filtered
|
? filtered
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a: any, b: any) =>
|
.sort((a: any, b: any) =>
|
||||||
String.prototype.localeCompare.call(
|
String.prototype.localeCompare.call(
|
||||||
a[this.sortAttr!],
|
a[this.sortAttr!],
|
||||||
b[this.sortAttr!],
|
b[this.sortAttr!],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: filtered;
|
: filtered;
|
||||||
this.output = sorted;
|
this.output = sorted;
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
append(v: T) {
|
append(v: T) {
|
||||||
this.data = [...this.data, v];
|
this.data = [...this.data, v];
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
const virtualizer = useVirtual({
|
const virtualizer = useVirtual({
|
||||||
size: dataSource.output.length,
|
size: dataSource.view.size,
|
||||||
parentRef,
|
parentRef,
|
||||||
useObserver: _testHeight
|
useObserver: _testHeight
|
||||||
? () => ({height: _testHeight, width: 1000})
|
? () => ({height: _testHeight, width: 1000})
|
||||||
@@ -148,7 +148,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.setOutputChangeListener((event) => {
|
dataSource.view.setListener((event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'reset':
|
case 'reset':
|
||||||
rerender(UpdatePrio.HIGH, true);
|
rerender(UpdatePrio.HIGH, true);
|
||||||
@@ -171,7 +171,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unmounted = true;
|
unmounted = true;
|
||||||
dataSource.setOutputChangeListener(undefined);
|
dataSource.view.setListener(undefined);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[dataSource, setForceUpdate, useFixedRowHeight, _testHeight],
|
[dataSource, setForceUpdate, useFixedRowHeight, _testHeight],
|
||||||
@@ -185,15 +185,15 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
useLayoutEffect(function updateWindow() {
|
useLayoutEffect(function updateWindow() {
|
||||||
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
||||||
const end = start + virtualizer.virtualItems.length;
|
const end = start + virtualizer.virtualItems.length;
|
||||||
if (start !== dataSource.windowStart && !followOutput.current) {
|
if (start !== dataSource.view.windowStart && !followOutput.current) {
|
||||||
onRangeChange?.(
|
onRangeChange?.(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
dataSource.output.length,
|
dataSource.view.size,
|
||||||
parentRef.current?.scrollTop ?? 0,
|
parentRef.current?.scrollTop ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
dataSource.setWindow(start, end);
|
dataSource.view.setWindow(start, end);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +223,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
useLayoutEffect(function scrollToEnd() {
|
useLayoutEffect(function scrollToEnd() {
|
||||||
if (followOutput.current) {
|
if (followOutput.current) {
|
||||||
virtualizer.scrollToIndex(
|
virtualizer.scrollToIndex(
|
||||||
dataSource.output.length - 1,
|
dataSource.view.size - 1,
|
||||||
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
|
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
|
||||||
{
|
{
|
||||||
align: 'end',
|
align: 'end',
|
||||||
@@ -255,7 +255,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
tabIndex={0}>
|
tabIndex={0}>
|
||||||
{virtualizer.virtualItems.map((virtualRow) => {
|
{virtualizer.virtualItems.map((virtualRow) => {
|
||||||
const entry = dataSource.getEntry(virtualRow.index);
|
const value = dataSource.view.get(virtualRow.index);
|
||||||
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
||||||
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
||||||
return (
|
return (
|
||||||
@@ -270,7 +270,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
}}
|
}}
|
||||||
ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
|
ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
|
||||||
{itemRenderer(entry.value, virtualRow.index, context)}
|
{itemRenderer(value, virtualRow.index, context)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export function DataTable<T extends object>(
|
|||||||
(e: React.KeyboardEvent<any>) => {
|
(e: React.KeyboardEvent<any>) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
const shiftPressed = e.shiftKey;
|
const shiftPressed = e.shiftKey;
|
||||||
const outputSize = dataSource.output.length;
|
const outputSize = dataSource.view.size;
|
||||||
const windowSize = virtualizerRef.current!.virtualItems.length;
|
const windowSize = virtualizerRef.current!.virtualItems.length;
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
@@ -244,7 +244,7 @@ export function DataTable<T extends object>(
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateFilter() {
|
function updateFilter() {
|
||||||
dataSource.setFilter(
|
dataSource.view.setFilter(
|
||||||
computeDataTableFilter(state.searchValue, state.columns),
|
computeDataTableFilter(state.searchValue, state.columns),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -257,11 +257,11 @@ export function DataTable<T extends object>(
|
|||||||
useEffect(
|
useEffect(
|
||||||
function updateSorting() {
|
function updateSorting() {
|
||||||
if (state.sorting === undefined) {
|
if (state.sorting === undefined) {
|
||||||
dataSource.setSortBy(undefined);
|
dataSource.view.setSortBy(undefined);
|
||||||
dataSource.setReversed(false);
|
dataSource.view.setReversed(false);
|
||||||
} else {
|
} else {
|
||||||
dataSource.setSortBy(state.sorting.key);
|
dataSource.view.setSortBy(state.sorting.key);
|
||||||
dataSource.setReversed(state.sorting.direction === 'desc');
|
dataSource.view.setReversed(state.sorting.direction === 'desc');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dataSource, state.sorting],
|
[dataSource, state.sorting],
|
||||||
@@ -342,7 +342,7 @@ export function DataTable<T extends object>(
|
|||||||
savePreferences(stateRef.current, lastOffset.current);
|
savePreferences(stateRef.current, lastOffset.current);
|
||||||
// if the component unmounts, we reset the SFRW pipeline to
|
// if the component unmounts, we reset the SFRW pipeline to
|
||||||
// avoid wasting resources in the background
|
// avoid wasting resources in the background
|
||||||
dataSource.reset();
|
dataSource.view.reset();
|
||||||
// clean ref
|
// clean ref
|
||||||
if (props.tableManagerRef) {
|
if (props.tableManagerRef) {
|
||||||
(props.tableManagerRef as MutableRefObject<any>).current = undefined;
|
(props.tableManagerRef as MutableRefObject<any>).current = undefined;
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export function getSelectedItem<T>(
|
|||||||
): T | undefined {
|
): T | undefined {
|
||||||
return selection.current < 0
|
return selection.current < 0
|
||||||
? undefined
|
? undefined
|
||||||
: dataSource.getItem(selection.current);
|
: dataSource.view.get(selection.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedItems<T>(
|
export function getSelectedItems<T>(
|
||||||
@@ -351,7 +351,7 @@ export function getSelectedItems<T>(
|
|||||||
): T[] {
|
): T[] {
|
||||||
return [...selection.items]
|
return [...selection.items]
|
||||||
.sort()
|
.sort()
|
||||||
.map((i) => dataSource.getItem(i))
|
.map((i) => dataSource.view.get(i))
|
||||||
.filter(Boolean) as any[];
|
.filter(Boolean) as any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user