Implemented sorting
Summary: For context see https://fb.workplace.com/notes/470523670998369 This diff adds the capability to apply a sorting, and inserts item in a sorted way using binary search in a temporarily intermediate collection. (That collection is optimized away in later diffs, so it is mostly the idea and the tests that are interesting) Reviewed By: nikoant Differential Revision: D25953336 fbshipit-source-id: a51b05e25242f0835280ada99798676311511ef0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
0dc1abdac4
commit
dfda71c350
@@ -12,6 +12,7 @@
|
|||||||
"@emotion/css": "^11.0.0",
|
"@emotion/css": "^11.0.0",
|
||||||
"@emotion/react": "^11.1.1",
|
"@emotion/react": "^11.1.1",
|
||||||
"immer": "^8.0.1",
|
"immer": "^8.0.1",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
"react-element-to-jsx-string": "^14.3.2"
|
"react-element-to-jsx-string": "^14.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {sortedIndexBy, sortedLastIndexBy, property} from 'lodash';
|
||||||
|
|
||||||
// TODO: support better minification
|
// TODO: support better minification
|
||||||
// TODO: separate views from datasource to be able to support multiple transformation simultanously
|
// TODO: separate views from datasource to be able to support multiple transformation simultanously
|
||||||
|
// TODO: expose interface with public members only
|
||||||
|
|
||||||
type ExtractKeyType<
|
type ExtractKeyType<
|
||||||
T extends object,
|
T extends object,
|
||||||
@@ -22,6 +25,7 @@ type AppendEvent<T> = {
|
|||||||
type UpdateEvent<T> = {
|
type UpdateEvent<T> = {
|
||||||
type: 'update';
|
type: 'update';
|
||||||
value: T;
|
value: T;
|
||||||
|
oldValue: T;
|
||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +41,9 @@ class DataSource<
|
|||||||
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();
|
||||||
dataUpdateQueue: DataEvent<T>[] = [];
|
dataUpdateQueue: DataEvent<T>[] = [];
|
||||||
// viewUpdateQueue;
|
|
||||||
|
private sortBy: undefined | ((a: T) => number | string);
|
||||||
|
private _sortedRecords: T[] | undefined;
|
||||||
|
|
||||||
viewRecords: T[] = [];
|
viewRecords: T[] = [];
|
||||||
nextViewRecords: T[] = []; // for double buffering
|
nextViewRecords: T[] = []; // for double buffering
|
||||||
@@ -64,8 +70,17 @@ class DataSource<
|
|||||||
return this._recordsById;
|
return this._recordsById;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed for testing only.
|
||||||
|
* Returns the set of records after applying sorting
|
||||||
|
*/
|
||||||
|
get sortedRecords(): readonly T[] {
|
||||||
|
return this.sortBy ? this._sortedRecords! : this._records;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(keyAttribute: KEY | undefined) {
|
constructor(keyAttribute: KEY | undefined) {
|
||||||
this.keyAttribute = keyAttribute;
|
this.keyAttribute = keyAttribute;
|
||||||
|
this.setSortBy(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertKeySet() {
|
private assertKeySet() {
|
||||||
@@ -127,7 +142,15 @@ class DataSource<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
update(index: number, value: T) {
|
update(index: number, value: T) {
|
||||||
|
const oldValue = this._records[index];
|
||||||
|
if (value === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.keyAttribute) {
|
if (this.keyAttribute) {
|
||||||
const key = this.getKey(value);
|
const key = this.getKey(value);
|
||||||
const currentKey = this.getKey(this._records[index]);
|
const currentKey = this.getKey(this._records[index]);
|
||||||
@@ -142,6 +165,7 @@ class DataSource<
|
|||||||
this.emitDataEvent({
|
this.emitDataEvent({
|
||||||
type: 'update',
|
type: 'update',
|
||||||
value,
|
value,
|
||||||
|
oldValue,
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -156,6 +180,25 @@ class DataSource<
|
|||||||
throw new Error('Not Implemented');
|
throw new Error('Not Implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSortBy(sortBy: undefined | keyof T | ((a: T) => number | string)) {
|
||||||
|
if (this.sortBy === sortBy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof sortBy === 'string') {
|
||||||
|
sortBy = property(sortBy); // TODO: it'd be great to recycle those if sortBy didn't change!
|
||||||
|
}
|
||||||
|
this.sortBy = sortBy as any;
|
||||||
|
if (sortBy === undefined) {
|
||||||
|
this._sortedRecords = undefined;
|
||||||
|
} else {
|
||||||
|
this._sortedRecords = [];
|
||||||
|
// TODO: using .sort will be faster?
|
||||||
|
this._records.forEach((value) => {
|
||||||
|
this.insertSorted(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitDataEvent(event: DataEvent<T>) {
|
emitDataEvent(event: DataEvent<T>) {
|
||||||
this.dataUpdateQueue.push(event);
|
this.dataUpdateQueue.push(event);
|
||||||
// TODO: schedule
|
// TODO: schedule
|
||||||
@@ -164,9 +207,69 @@ class DataSource<
|
|||||||
|
|
||||||
processEvents() {
|
processEvents() {
|
||||||
const events = this.dataUpdateQueue.splice(0);
|
const events = this.dataUpdateQueue.splice(0);
|
||||||
events.forEach((_event) => {
|
events.forEach(this.processEvent);
|
||||||
// TODO:
|
}
|
||||||
});
|
|
||||||
|
processEvent = (event: DataEvent<T>) => {
|
||||||
|
const {value} = event;
|
||||||
|
switch (event.type) {
|
||||||
|
case 'append':
|
||||||
|
// sort
|
||||||
|
if (this.sortBy) {
|
||||||
|
this.insertSorted(value);
|
||||||
|
}
|
||||||
|
// reverse
|
||||||
|
|
||||||
|
// filter
|
||||||
|
|
||||||
|
// notify
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
// sort
|
||||||
|
if (this.sortBy) {
|
||||||
|
// find old entry
|
||||||
|
const oldIndex = this.getSortedIndex(event.oldValue);
|
||||||
|
if (
|
||||||
|
this.sortBy(this._sortedRecords![oldIndex]) === this.sortBy(value)
|
||||||
|
) {
|
||||||
|
// sort value is the same? just swap the item
|
||||||
|
this._sortedRecords![oldIndex] = value;
|
||||||
|
} else {
|
||||||
|
// sort value is different? remove and add
|
||||||
|
this._sortedRecords!.splice(oldIndex, 1);
|
||||||
|
this.insertSorted(value);
|
||||||
|
}
|
||||||
|
// reverse
|
||||||
|
|
||||||
|
// filter
|
||||||
|
|
||||||
|
// notify
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown event type');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getSortedIndex(value: T) {
|
||||||
|
let index = sortedIndexBy(this._sortedRecords, value, this.sortBy);
|
||||||
|
// the item we are looking for is not necessarily the first one at the insertion index
|
||||||
|
while (this._sortedRecords![index] !== value) {
|
||||||
|
index++;
|
||||||
|
if (index >= this._sortedRecords!.length) {
|
||||||
|
throw new Error('illegal state: sortedIndex not found'); // sanity check to avoid browser freeze if people mess up with internals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private insertSorted(value: T) {
|
||||||
|
const insertionIndex = sortedLastIndexBy(
|
||||||
|
this._sortedRecords,
|
||||||
|
value,
|
||||||
|
this.sortBy!,
|
||||||
|
);
|
||||||
|
this._sortedRecords!.splice(insertionIndex, 0, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,3 +94,74 @@ test('throws on invalid keys', () => {
|
|||||||
ds.append({id: 'cookie', title: 'test'});
|
ds.append({id: 'cookie', title: 'test'});
|
||||||
}).toThrow(`Duplicate key: 'cookie'`);
|
}).toThrow(`Duplicate key: 'cookie'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sorting works', () => {
|
||||||
|
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
|
||||||
|
ds.setSortBy((todo) => todo.title);
|
||||||
|
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
|
ds.setSortBy(undefined);
|
||||||
|
ds.setSortBy(undefined);
|
||||||
|
expect(ds.sortedRecords).toEqual([eatCookie, drinkCoffee]);
|
||||||
|
ds.setSortBy((todo) => todo.title);
|
||||||
|
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
|
||||||
|
|
||||||
|
const aleph = {
|
||||||
|
id: 'd',
|
||||||
|
title: 'aleph',
|
||||||
|
};
|
||||||
|
ds.append(aleph);
|
||||||
|
expect(ds.records).toEqual([eatCookie, drinkCoffee, aleph]);
|
||||||
|
expect(ds.sortedRecords).toEqual([aleph, drinkCoffee, eatCookie]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sorting preserves insertion order with equal keys', () => {
|
||||||
|
type N = {
|
||||||
|
$: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const a = {$: 'a', name: 'a'};
|
||||||
|
const b1 = {$: 'b', name: 'b1'};
|
||||||
|
const b2 = {$: 'b', name: 'b2'};
|
||||||
|
const b3 = {$: 'b', name: 'b3'};
|
||||||
|
const c = {$: 'c', name: 'c'};
|
||||||
|
|
||||||
|
const ds = createDataSource<N>([]);
|
||||||
|
ds.setSortBy('$');
|
||||||
|
ds.append(b1);
|
||||||
|
ds.append(c);
|
||||||
|
ds.append(b2);
|
||||||
|
ds.append(a);
|
||||||
|
ds.append(b3);
|
||||||
|
|
||||||
|
expect(ds.records).toEqual([b1, c, b2, a, b3]);
|
||||||
|
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, c]);
|
||||||
|
|
||||||
|
// if we append a new item with existig item, it should end up in the end
|
||||||
|
const b4 = {
|
||||||
|
$: 'b',
|
||||||
|
name: 'b4',
|
||||||
|
};
|
||||||
|
ds.append(b4);
|
||||||
|
expect(ds.records).toEqual([b1, c, b2, a, b3, b4]);
|
||||||
|
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, b4, c]);
|
||||||
|
|
||||||
|
// if we replace the middle item, it should end up in the middle
|
||||||
|
const b2r = {
|
||||||
|
$: 'b',
|
||||||
|
name: 'b2replacement',
|
||||||
|
};
|
||||||
|
ds.update(2, b2r);
|
||||||
|
expect(ds.records).toEqual([b1, c, b2r, a, b3, b4]);
|
||||||
|
expect(ds.sortedRecords).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
|
||||||
|
const b3r = {
|
||||||
|
$: 'aa',
|
||||||
|
name: 'b3replacement',
|
||||||
|
};
|
||||||
|
ds.update(4, b3r);
|
||||||
|
expect(ds.records).toEqual([b1, c, b2r, a, b3r, b4]);
|
||||||
|
expect(ds.sortedRecords).toEqual([a, b3r, b1, b2r, b4, c]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user