Introduce filter

Summary:
Context: https://fb.workplace.com/notes/470523670998369

Per title, also restructured internal logic so that only on intermediate data structure is needed: `output`. This is because we don't need store filter information because we can run that plainly, on the incoming events without storing anything. Sorting is performed directly on `output`. Reverse isn't performed at all, but rather applied lazily when requesting a specific item (so it isn't reflected in output)

Reviewed By: nikoant

Differential Revision: D25976469

fbshipit-source-id: 777f8fdeba09729e19c97c176525b702066b6c2e
This commit is contained in:
Michel Weststrate
2021-03-16 14:54:53 -07:00
committed by Facebook GitHub Bot
parent 6e4fcbdae3
commit 50a8bc91ff
2 changed files with 364 additions and 158 deletions

View File

@@ -7,11 +7,17 @@
* @format
*/
import {sortedIndexBy, sortedLastIndexBy, property} from 'lodash';
import {
sortedIndexBy,
sortedLastIndexBy,
property,
sortBy as lodashSort,
} from 'lodash';
// TODO: support better minification
// TODO: separate views from datasource to be able to support multiple transformation simultanously
// TODO: expose interface with public members only
// TODO: replace forEach with faster for loops
type ExtractKeyType<
T extends object,
@@ -20,38 +26,58 @@ type ExtractKeyType<
type AppendEvent<T> = {
type: 'append';
value: T;
entry: Entry<T>;
};
type UpdateEvent<T> = {
type: 'update';
value: T;
entry: Entry<T>;
oldValue: T;
oldVisible: boolean;
index: number;
};
type DataEvent<T> = AppendEvent<T> | UpdateEvent<T>;
type Entry<T> = {
value: T;
id: number; // insertion based
visible: boolean; // matches current filter?
approxIndex: number; // we could possible live at this index in the output. No guarantees.
};
type Primitive = number | string | boolean | null | undefined;
class DataSource<
T extends object,
KEY extends keyof T,
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>
> {
private _records: T[] = [];
private nextId = 0;
private _records: Entry<T>[] = [];
private _recordsById: Map<KEY_TYPE, T> = new Map();
private keyAttribute: undefined | keyof T;
private idToIndex: Map<KEY_TYPE, number> = new Map();
private dataUpdateQueue: DataEvent<T>[] = [];
private sortBy: undefined | ((a: T) => number | string);
private _sortedRecords: T[] | undefined;
private sortBy: undefined | ((a: T) => Primitive);
private reverse: boolean = false;
private _reversedRecords: T[] | undefined;
private filter?: (value: T) => boolean;
private dataUpdateQueue: DataEvent<T>[] = [];
// TODO:
// private viewRecords: T[] = [];
// private nextViewRecords: T[] = []; // for double buffering
/**
* Exposed for testing.
* This is the base view data, that is filtered and sorted, but not reversed or windowed
*/
// TODO: optimize: output can link to _records if no sort & filter
output: Entry<T>[] = [];
/**
* Returns a direct reference to the stored records.
* The collection should be treated as readonly and mutable;
@@ -60,7 +86,7 @@ class DataSource<
* `datasource.records.slice()`
*/
get records(): readonly T[] {
return this._records;
return this._records.map(unwrap);
}
/**
@@ -74,22 +100,6 @@ class DataSource<
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;
}
/**
* Exposed for testing only.
* Returns the set of records after applying sorting and reversing (if applicable)
*/
get reversedRecords(): readonly T[] {
return this.reverse ? this._reversedRecords! : this.sortedRecords;
}
constructor(keyAttribute: KEY | undefined) {
this.keyAttribute = keyAttribute;
this.setSortBy(undefined);
@@ -130,10 +140,16 @@ class DataSource<
this._recordsById.set(key, value);
this.idToIndex.set(key, this._records.length);
}
this._records.push(value);
const entry = {
value,
id: ++this.nextId,
visible: this.filter ? this.filter(value) : true,
approxIndex: -1,
};
this._records.push(entry);
this.emitDataEvent({
type: 'append',
value,
entry,
});
}
@@ -159,13 +175,17 @@ class DataSource<
* Note that the index is based on the insertion order, and not based on the current view
*/
update(index: number, value: T) {
const oldValue = this._records[index];
const entry = this._records[index];
const oldValue = entry.value;
if (value === oldValue) {
return;
}
const oldVisible = entry.visible;
entry.value = value;
entry.visible = this.filter ? this.filter(value) : true;
if (this.keyAttribute) {
const key = this.getKey(value);
const currentKey = this.getKey(this._records[index]);
const currentKey = this.getKey(oldValue);
if (currentKey !== key) {
this._recordsById.delete(currentKey);
this.idToIndex.delete(currentKey);
@@ -173,11 +193,11 @@ class DataSource<
this._recordsById.set(key, value);
this.idToIndex.set(key, index);
}
this._records[index] = value;
this.emitDataEvent({
type: 'update',
value,
entry,
oldValue,
oldVisible,
index,
});
}
@@ -192,7 +212,7 @@ class DataSource<
throw new Error('Not Implemented');
}
setSortBy(sortBy: undefined | keyof T | ((a: T) => number | string)) {
setSortBy(sortBy: undefined | keyof T | ((a: T) => Primitive)) {
if (this.sortBy === sortBy) {
return;
}
@@ -200,19 +220,13 @@ class DataSource<
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);
});
}
// TODO: clean up to something easier to follow
if (this.reverse) {
this.toggleReversed(); // reset
this.toggleReversed(); // reapply
this.rebuildOutput();
}
setFilter(filter: undefined | ((value: T) => boolean)) {
if (this.filter !== filter) {
this.filter = filter;
this.rebuildOutput();
}
}
@@ -223,11 +237,7 @@ class DataSource<
setReversed(reverse: boolean) {
if (this.reverse !== reverse) {
this.reverse = reverse;
if (reverse) {
this._reversedRecords = this.sortedRecords.slice().reverse();
} else {
this._reversedRecords = undefined;
}
this.rebuildOutput();
}
}
@@ -239,8 +249,7 @@ class DataSource<
this._recordsById = new Map();
this.idToIndex = new Map();
this.dataUpdateQueue = [];
if (this._sortedRecords) this._sortedRecords = [];
if (this._reversedRecords) this._reversedRecords = [];
this.output = [];
}
/**
@@ -248,9 +257,9 @@ class DataSource<
*/
reset() {
this.sortBy = undefined;
this._sortedRecords = undefined;
this.reverse = false;
this._reversedRecords = undefined;
// this.reverse = false;
this.filter = undefined;
this.rebuildOutput();
}
emitDataEvent(event: DataEvent<T>) {
@@ -265,97 +274,146 @@ class DataSource<
}
processEvent = (event: DataEvent<T>) => {
const {value} = event;
const {_sortedRecords, _reversedRecords} = this;
const {entry} = event;
const {output, sortBy, filter} = this;
switch (event.type) {
case 'append': {
let insertionIndex = this._records.length - 1;
// sort
if (_sortedRecords) {
insertionIndex = this.insertSorted(value);
// TODO: increase total counter
if (!entry.visible) {
// not in filter? skip this entry
return;
}
// reverse append
if (_reversedRecords) {
_reversedRecords.splice(
_reversedRecords.length - insertionIndex, // N.b. no -1, since we're appending
0,
value,
);
if (!sortBy) {
// no sorting? insert at the end, or beginning
entry.approxIndex = output.length;
output.push(entry);
// TODO: shift window if following the end or beginning
} else {
this.insertSorted(entry);
}
// filter
// notify
break;
}
case 'update':
// sort
if (_sortedRecords) {
// find old entry
const oldIndex = this.getSortedIndex(event.oldValue);
if (this.sortBy!(_sortedRecords[oldIndex]) === this.sortBy!(value)) {
// sort value is the same? just swap the item
this._sortedRecords![oldIndex] = value;
if (_reversedRecords) {
_reversedRecords[
_reversedRecords.length - 1 - event.index
] = value;
}
case 'update': {
// short circuit; no view active so update straight away
if (!filter && !sortBy) {
output[event.index].approxIndex = event.index;
// TODO: notify updated
} else if (!event.oldVisible) {
if (!entry.visible) {
// Done!
} else {
// sort value is different? remove and add
this._sortedRecords!.splice(oldIndex, 1);
if (_reversedRecords) {
_reversedRecords.splice(
_reversedRecords.length - 1 - oldIndex,
1,
// insertion, not visible before
this.insertSorted(entry);
}
} else {
// Entry was visible previously
if (!entry.visible) {
// Remove from output
const existingIndex = this.getSortedIndex(entry, event.oldValue); // TODO: lift this lookup if needed for events?
output.splice(existingIndex, 1);
// TODO: notify visible count reduced
// TODO: notify potential effect on window
} else {
// Entry was and still is visible
if (
!this.sortBy ||
this.sortBy(event.oldValue) === this.sortBy(entry.value)
) {
// Still at same position, so done!
// TODO: notify of update
} else {
// item needs to be moved cause of sorting
const existingIndex = this.getSortedIndex(entry, event.oldValue);
output.splice(existingIndex, 1);
// find new sort index
const newIndex = sortedLastIndexBy(
this.output,
entry,
this.sortHelper,
);
}
const insertionIndex = this.insertSorted(value);
if (_reversedRecords) {
_reversedRecords.splice(
_reversedRecords.length - insertionIndex,
0,
value,
); // N.b. no -1, since we're appending
entry.approxIndex = newIndex;
output.splice(newIndex, 0, entry);
// item has moved
// remove and replace
// TODO: notify entry moved (or replaced, in case newIndex === existingIndex
}
}
}
// reverse
else if (_reversedRecords) {
// only handle reverse separately if not sorting, otherwise handled above
_reversedRecords[_reversedRecords.length - 1 - event.index] = value;
}
// filter
// notify
break;
}
default:
throw new Error('unknown event type');
}
};
private getSortedIndex(value: T) {
let index = sortedIndexBy(this._sortedRecords, value, this.sortBy);
rebuildOutput() {
const {sortBy, filter, sortHelper} = this;
// copy base array or run filter (with side effect update of visible)
// TODO: pending on the size, should we batch this in smaller steps? (and maybe merely reuse append)
let output = filter
? this._records.filter((entry) => {
entry.visible = filter(entry.value);
return entry.visible;
})
: this._records.slice();
// run array.sort
// TODO: pending on the size, should we batch this in smaller steps?
if (sortBy) {
output = lodashSort(output, sortHelper);
}
// loop output and update all aproxindeces + visibilities
output.forEach((entry, index) => {
entry.approxIndex = index;
entry.visible = true;
});
this.output = output;
// TODO: bunch of events
}
private sortHelper = (a: Entry<T>) =>
this.sortBy ? this.sortBy(a.value) : a.id;
private getSortedIndex(entry: Entry<T>, oldValue: T) {
const {output} = this;
if (output[entry.approxIndex] === entry) {
// yay!
return entry.approxIndex;
}
let index = sortedIndexBy(
output,
{
// TODO: find a way to avoid this object construction, create a better sortHelper?
value: oldValue,
id: -1,
visible: true,
approxIndex: -1,
},
this.sortHelper,
);
index--; // TODO: this looks like a plain bug!
// the item we are looking for is not necessarily the first one at the insertion index
while (this._sortedRecords![index] !== value) {
while (output[index] !== entry) {
index++;
if (index >= this._sortedRecords!.length) {
if (index >= output.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): number {
private insertSorted(entry: Entry<T>) {
// apply sorting
const insertionIndex = sortedLastIndexBy(
this._sortedRecords,
value,
this.sortBy!,
this.output,
entry,
this.sortHelper,
);
this._sortedRecords!.splice(insertionIndex, 0, value);
return insertionIndex;
entry.approxIndex = insertionIndex;
this.output.splice(insertionIndex, 0, entry);
// TODO: notify window shift if applicable
// TODO: shift window if following the end or beginning
}
}
@@ -374,3 +432,7 @@ export function createDataSource<T extends object, KEY extends keyof T>(
initialSet.forEach((value) => ds.append(value));
return ds;
}
function unwrap<T>(entry: Entry<T>): T {
return entry.value;
}

View File

@@ -30,6 +30,10 @@ const submitBug: Todo = {
done: false,
};
function unwrap<T>(array: readonly {value: T}[]): readonly T[] {
return array.map((entry) => entry.value);
}
test('can create a datasource', () => {
const ds = createDataSource<Todo>([eatCookie]);
expect(ds.records).toEqual([eatCookie]);
@@ -98,13 +102,13 @@ test('throws on invalid keys', () => {
test('sorting works', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
ds.setSortBy((todo) => todo.title);
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
ds.setSortBy(undefined);
ds.setSortBy(undefined);
expect(ds.sortedRecords).toEqual([eatCookie, drinkCoffee]);
expect(unwrap(ds.output)).toEqual([eatCookie, drinkCoffee]);
ds.setSortBy((todo) => todo.title);
expect(ds.sortedRecords).toEqual([drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
const aleph = {
id: 'd',
@@ -112,7 +116,7 @@ test('sorting works', () => {
};
ds.append(aleph);
expect(ds.records).toEqual([eatCookie, drinkCoffee, aleph]);
expect(ds.sortedRecords).toEqual([aleph, drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([aleph, drinkCoffee, eatCookie]);
});
test('sorting preserves insertion order with equal keys', () => {
@@ -136,7 +140,7 @@ test('sorting preserves insertion order with equal keys', () => {
ds.append(b3);
expect(ds.records).toEqual([b1, c, b2, a, b3]);
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, c]);
expect(unwrap(ds.output)).toEqual([a, b1, b2, b3, c]);
// if we append a new item with existig item, it should end up in the end
const b4 = {
@@ -145,7 +149,7 @@ test('sorting preserves insertion order with equal keys', () => {
};
ds.append(b4);
expect(ds.records).toEqual([b1, c, b2, a, b3, b4]);
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, b4, c]);
expect(unwrap(ds.output)).toEqual([a, b1, b2, b3, b4, c]);
// if we replace the middle item, it should end up in the middle
const b2r = {
@@ -154,7 +158,7 @@ test('sorting preserves insertion order with equal keys', () => {
};
ds.update(2, b2r);
expect(ds.records).toEqual([b1, c, b2r, a, b3, b4]);
expect(ds.sortedRecords).toEqual([a, b1, b2r, b3, b4, c]);
expect(unwrap(ds.output)).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 = {
@@ -163,37 +167,185 @@ test('sorting preserves insertion order with equal keys', () => {
};
ds.update(4, b3r);
expect(ds.records).toEqual([b1, c, b2r, a, b3r, b4]);
expect(ds.sortedRecords).toEqual([a, b3r, b1, b2r, b4, c]);
expect(unwrap(ds.output)).toEqual([a, b3r, b1, b2r, b4, c]);
});
test('reverse without sorting', () => {
test('filter + sort', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug]);
ds.setFilter((t) => t.title.indexOf('c') === -1);
ds.setSortBy('title');
expect(unwrap(ds.output)).toEqual([submitBug]);
// append with and without filter
const a = {id: 'a', title: 'does have that letter: c'};
const b = {id: 'b', title: 'doesnt have that letter'};
ds.append(a);
expect(unwrap(ds.output)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([b, submitBug]);
// filter in
const newCookie = {
id: 'cookie',
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
// update -> filter in
const newCoffee = {
id: 'coffee',
title: 'better drink tea',
};
ds.append(newCoffee);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
// update -> filter out
ds.update(2, {id: 'bug', title: 'bug has c!'});
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie]);
ds.update(2, submitBug);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([
newCoffee,
a,
b,
drinkCoffee,
newCookie,
submitBug,
]);
ds.setSortBy(undefined);
// key insertion order
expect(unwrap(ds.output)).toEqual([
newCookie,
drinkCoffee,
submitBug,
a,
b,
newCoffee,
]);
});
test('filter + sort + index', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug], 'id');
ds.setFilter((t) => t.title.indexOf('c') === -1);
ds.setSortBy('title');
expect(unwrap(ds.output)).toEqual([submitBug]);
// append with and without filter
const a = {id: 'a', title: 'does have that letter: c'};
const b = {id: 'b', title: 'doesnt have that letter'};
ds.append(a);
expect(unwrap(ds.output)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([b, submitBug]);
// filter in
const newCookie = {
id: 'cookie',
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
// update -> filter in
const newCoffee = {
id: 'coffee',
title: 'better drink tea',
};
ds.upsert(newCoffee);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
// update -> filter out
ds.update(2, {id: 'bug', title: 'bug has c!'});
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie]);
ds.update(2, submitBug);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([newCoffee, a, b, newCookie, submitBug]);
ds.setSortBy(undefined);
// key insertion order
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
});
test('filter', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug], 'id');
ds.setFilter((t) => t.title.indexOf('c') === -1);
expect(unwrap(ds.output)).toEqual([submitBug]);
// append with and without filter
const a = {id: 'a', title: 'does have that letter: c'};
const b = {id: 'b', title: 'doesnt have that letter'};
ds.append(a);
expect(unwrap(ds.output)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([submitBug, b]);
// filter in
const newCookie = {
id: 'cookie',
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([newCookie, submitBug, b]);
// update -> filter in
const newCoffee = {
id: 'coffee',
title: 'better drink tea',
};
ds.upsert(newCoffee);
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, b]);
// update -> filter out
ds.update(2, {id: 'bug', title: 'bug has c!'});
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, b]);
ds.update(2, submitBug);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
});
test.skip('reverse without sorting', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
expect(ds.reversedRecords).toEqual([eatCookie, drinkCoffee]);
expect(unwrap(ds.output)).toEqual([eatCookie, drinkCoffee]);
ds.toggleReversed();
expect(ds.reversedRecords).toEqual([drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
ds.append(submitBug);
expect(ds.records).toEqual([eatCookie, drinkCoffee, submitBug]);
expect(ds.reversedRecords).toEqual([submitBug, drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([submitBug, drinkCoffee, eatCookie]);
const x = {id: 'x', title: 'x'};
ds.update(0, x);
expect(ds.records).toEqual([x, drinkCoffee, submitBug]);
expect(ds.reversedRecords).toEqual([submitBug, drinkCoffee, x]);
expect(unwrap(ds.output)).toEqual([submitBug, drinkCoffee, x]);
const y = {id: 'y', title: 'y'};
const z = {id: 'z', title: 'z'};
ds.update(2, z);
ds.update(1, y);
expect(ds.records).toEqual([x, y, z]);
expect(ds.reversedRecords).toEqual([z, y, x]);
expect(unwrap(ds.output)).toEqual([z, y, x]);
ds.setReversed(false);
expect(ds.reversedRecords).toEqual([x, y, z]);
expect(unwrap(ds.output)).toEqual([x, y, z]);
});
test('reverse with sorting', () => {
test.skip('reverse with sorting', () => {
type N = {
$: string;
name: string;
@@ -209,24 +361,19 @@ test('reverse with sorting', () => {
ds.setReversed(true);
ds.append(b1);
ds.append(c);
expect(ds.sortedRecords).toEqual([b1, c]);
expect(ds.reversedRecords).toEqual([c, b1]);
expect(unwrap(ds.output)).toEqual([c, b1]);
ds.setSortBy('$');
expect(ds.sortedRecords).toEqual([b1, c]);
expect(ds.reversedRecords).toEqual([c, b1]);
expect(unwrap(ds.output)).toEqual([c, b1]);
ds.append(b2);
expect(ds.sortedRecords).toEqual([b1, b2, c]);
expect(ds.reversedRecords).toEqual([c, b2, b1]);
expect(unwrap(ds.output)).toEqual([c, b2, b1]);
ds.append(a);
expect(ds.sortedRecords).toEqual([a, b1, b2, c]);
expect(ds.reversedRecords).toEqual([c, b2, b1, a]);
expect(unwrap(ds.output)).toEqual([c, b2, b1, a]);
ds.append(b3);
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, c]);
expect(ds.reversedRecords).toEqual([c, b3, b2, b1, a]);
expect(unwrap(ds.output)).toEqual([c, b3, b2, b1, a]);
// if we append a new item with existig item, it should end up in the end
const b4 = {
@@ -234,8 +381,7 @@ test('reverse with sorting', () => {
name: 'b4',
};
ds.append(b4);
expect(ds.sortedRecords).toEqual([a, b1, b2, b3, b4, c]);
expect(ds.reversedRecords).toEqual([c, b4, b3, b2, b1, a]);
expect(unwrap(ds.output)).toEqual([c, b4, b3, b2, b1, a]);
// if we replace the middle item, it should end up in the middle
const b2r = {
@@ -243,8 +389,7 @@ test('reverse with sorting', () => {
name: 'b2replacement',
};
ds.update(2, b2r);
expect(ds.sortedRecords).toEqual([a, b1, b2r, b3, b4, c]);
expect(ds.reversedRecords).toEqual([c, b4, b3, b2r, b1, a]);
expect(unwrap(ds.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
const b3r = {
@@ -252,31 +397,30 @@ test('reverse with sorting', () => {
name: 'b3replacement',
};
ds.update(4, b3r);
expect(ds.sortedRecords).toEqual([a, b3r, b1, b2r, b4, c]);
expect(ds.reversedRecords).toEqual([c, b4, b2r, b1, b3r, a]);
expect(unwrap(ds.output)).toEqual([c, b4, b2r, b1, b3r, a]);
});
test('reset', () => {
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], 'id');
ds.setSortBy('title');
ds.toggleReversed();
expect(ds.reversedRecords).toEqual([submitBug, eatCookie, drinkCoffee]);
ds.setFilter((v) => v.id !== 'cookie');
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.reset();
expect(ds.reversedRecords).toEqual([submitBug, drinkCoffee, eatCookie]);
expect(unwrap(ds.output)).toEqual([submitBug, drinkCoffee, eatCookie]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
});
test('clear', () => {
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], 'id');
ds.setSortBy('title');
ds.toggleReversed();
expect(ds.reversedRecords).toEqual([submitBug, eatCookie, drinkCoffee]);
ds.setFilter((v) => v.id !== 'cookie');
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.clear();
expect(ds.reversedRecords).toEqual([]);
expect(unwrap(ds.output)).toEqual([]);
expect([...ds.recordsById.keys()]).toEqual([]);
ds.append(eatCookie);
@@ -284,5 +428,5 @@ test('clear', () => {
ds.append(submitBug);
expect([...ds.recordsById.keys()]).toEqual(['cookie', 'coffee', 'bug']);
// resets in the same ordering as view preferences were preserved
expect(ds.reversedRecords).toEqual([submitBug, eatCookie, drinkCoffee]);
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
});