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:
Michel Weststrate
2021-03-16 14:54:53 -07:00
committed by Facebook GitHub Bot
parent d73f6578a7
commit 602152665b
7 changed files with 611 additions and 466 deletions

View File

@@ -34,43 +34,52 @@ function unwrap<T>(array: readonly {value: T}[]): readonly T[] {
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', () => {
const ds = createDataSource<Todo>([eatCookie]);
expect(ds.records).toEqual([eatCookie]);
expect(ds.records()).toEqual([eatCookie]);
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);
expect(ds.records[1]).toBe(submitBug);
expect(ds.records()[1]).toBe(submitBug);
ds.remove(0);
expect(ds.records[0]).toBe(submitBug);
ds.delete(0);
expect(ds.records()[0]).toBe(submitBug);
});
test('can create a keyed datasource', () => {
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
expect(ds.records).toEqual([eatCookie]);
expect(ds.records()).toEqual([eatCookie]);
ds.append(drinkCoffee);
expect(ds.records).toEqual([eatCookie, drinkCoffee]);
expect(ds.records()).toEqual([eatCookie, drinkCoffee]);
expect(ds.recordsById.get('bug')).toBe(undefined);
expect(ds.recordsById.get('cookie')).toBe(eatCookie);
expect(ds.recordsById.get('coffee')).toBe(drinkCoffee);
expect(ds.indexOfKey('bug')).toBe(-1);
expect(ds.indexOfKey('cookie')).toBe(0);
expect(ds.indexOfKey('coffee')).toBe(1);
expect(ds.getById('bug')).toBe(undefined);
expect(ds.getById('cookie')).toBe(eatCookie);
expect(ds.getById('coffee')).toBe(drinkCoffee);
expect(ds.getIndexOfKey('bug')).toBe(-1);
expect(ds.getIndexOfKey('cookie')).toBe(0);
expect(ds.getIndexOfKey('coffee')).toBe(1);
ds.update(1, submitBug);
expect(ds.records[1]).toBe(submitBug);
expect(ds.recordsById.get('coffee')).toBe(undefined);
expect(ds.recordsById.get('bug')).toBe(submitBug);
expect(ds.indexOfKey('bug')).toBe(1);
expect(ds.indexOfKey('cookie')).toBe(0);
expect(ds.indexOfKey('coffee')).toBe(-1);
expect(ds.records()[1]).toBe(submitBug);
expect(ds.getById('coffee')).toBe(undefined);
expect(ds.getById('bug')).toBe(submitBug);
expect(ds.getIndexOfKey('bug')).toBe(1);
expect(ds.getIndexOfKey('cookie')).toBe(0);
expect(ds.getIndexOfKey('coffee')).toBe(-1);
// upsert existing
const newBug = {
@@ -79,8 +88,8 @@ test('can create a keyed datasource', () => {
done: true,
};
ds.upsert(newBug);
expect(ds.records[1]).toBe(newBug);
expect(ds.recordsById.get('bug')).toBe(newBug);
expect(ds.records()[1]).toBe(newBug);
expect(ds.getById('bug')).toBe(newBug);
// upsert new
const trash = {
@@ -88,16 +97,16 @@ test('can create a keyed datasource', () => {
title: 'take trash out',
};
ds.upsert(trash);
expect(ds.records[2]).toBe(trash);
expect(ds.recordsById.get('trash')).toBe(trash);
expect(ds.records()[2]).toBe(trash);
expect(ds.getById('trash')).toBe(trash);
// delete by key
expect(ds.records).toEqual([eatCookie, newBug, trash]);
expect(ds.removeByKey('bug')).toBe(true);
expect(ds.records).toEqual([eatCookie, trash]);
expect(ds.indexOfKey('bug')).toBe(-1);
expect(ds.indexOfKey('cookie')).toBe(0);
expect(ds.indexOfKey('trash')).toBe(1);
expect(ds.records()).toEqual([eatCookie, newBug, trash]);
expect(ds.deleteByKey('bug')).toBe(true);
expect(ds.records()).toEqual([eatCookie, trash]);
expect(ds.getIndexOfKey('bug')).toBe(-1);
expect(ds.getIndexOfKey('cookie')).toBe(0);
expect(ds.getIndexOfKey('trash')).toBe(1);
});
test('throws on invalid keys', () => {
@@ -110,32 +119,41 @@ test('throws on invalid keys', () => {
}).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', () => {
const ds = createDataSource<Todo>([eatCookie], {key: 'id'});
expect(ds.removeByKey('trash')).toBe(false);
expect(ds.deleteByKey('trash')).toBe(false);
expect(() => {
ds.remove(1);
ds.delete(1);
}).toThrowError('Out of bounds');
});
test('sorting works', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
ds.setSortBy((todo) => todo.title);
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
ds.view.setSortBy((todo) => todo.title);
expect(rawOutput(ds)).toEqual([drinkCoffee, eatCookie]);
ds.setSortBy(undefined);
ds.setSortBy(undefined);
expect(unwrap(ds.output)).toEqual([eatCookie, drinkCoffee]);
ds.setSortBy((todo) => todo.title);
expect(unwrap(ds.output)).toEqual([drinkCoffee, eatCookie]);
ds.view.setSortBy(undefined);
ds.view.setSortBy(undefined);
expect(rawOutput(ds)).toEqual([eatCookie, drinkCoffee]);
ds.view.setSortBy((todo) => todo.title);
expect(rawOutput(ds)).toEqual([drinkCoffee, eatCookie]);
const aleph = {
id: 'd',
title: 'aleph',
};
ds.append(aleph);
expect(ds.records).toEqual([eatCookie, drinkCoffee, aleph]);
expect(unwrap(ds.output)).toEqual([aleph, drinkCoffee, eatCookie]);
expect(ds.records()).toEqual([eatCookie, drinkCoffee, aleph]);
expect(rawOutput(ds)).toEqual([aleph, drinkCoffee, eatCookie]);
});
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 ds = createDataSource<N>([]);
ds.setSortBy('$');
ds.view.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(unwrap(ds.output)).toEqual([a, b1, b2, b3, c]);
expect(ds.records()).toEqual([b1, c, b2, a, b3]);
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
const b4 = {
@@ -167,8 +185,8 @@ test('sorting preserves insertion order with equal keys', () => {
name: 'b4',
};
ds.append(b4);
expect(ds.records).toEqual([b1, c, b2, a, b3, b4]);
expect(unwrap(ds.output)).toEqual([a, b1, b2, b3, b4, c]);
expect(ds.records()).toEqual([b1, c, b2, a, b3, b4]);
expect(rawOutput(ds)).toEqual([a, b1, b2, b3, b4, c]);
// if we replace the middle item, it should end up in the middle
const b2r = {
@@ -176,8 +194,8 @@ test('sorting preserves insertion order with equal keys', () => {
name: 'b2replacement',
};
ds.update(2, b2r);
expect(ds.records).toEqual([b1, c, b2r, a, b3, b4]);
expect(unwrap(ds.output)).toEqual([a, b1, b2r, b3, b4, c]);
expect(ds.records()).toEqual([b1, c, b2r, a, b3, b4]);
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
const b3r = {
@@ -185,29 +203,29 @@ test('sorting preserves insertion order with equal keys', () => {
name: 'b3replacement',
};
ds.update(4, b3r);
expect(ds.records).toEqual([b1, c, b2r, a, b3r, b4]);
expect(unwrap(ds.output)).toEqual([a, b3r, b1, b2r, b4, c]);
expect(ds.records()).toEqual([b1, c, b2r, a, b3r, b4]);
expect(rawOutput(ds)).toEqual([a, b3r, b1, b2r, b4, c]);
ds.remove(3);
expect(ds.records).toEqual([b1, c, b2r, b3r, b4]);
expect(unwrap(ds.output)).toEqual([b3r, b1, b2r, b4, c]);
ds.delete(3);
expect(ds.records()).toEqual([b1, c, b2r, b3r, b4]);
expect(rawOutput(ds)).toEqual([b3r, b1, b2r, b4, c]);
});
test('filter + sort', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee, submitBug]);
ds.setFilter((t) => t.title.indexOf('c') === -1);
ds.setSortBy('title');
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
ds.view.setSortBy('title');
expect(unwrap(ds.output)).toEqual([submitBug]);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([b, submitBug]);
expect(rawOutput(ds)).toEqual([b, submitBug]);
// filter in
const newCookie = {
@@ -215,7 +233,7 @@ test('filter + sort', () => {
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
expect(rawOutput(ds)).toEqual([b, newCookie, submitBug]);
// update -> filter in
const newCoffee = {
@@ -223,35 +241,25 @@ test('filter + sort', () => {
title: 'better drink tea',
};
ds.append(newCoffee);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie]);
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.remove(3); // b
expect(unwrap(ds.output)).toEqual([newCoffee, newCookie, submitBug]);
ds.delete(3); // a
ds.delete(3); // b
expect(rawOutput(ds)).toEqual([newCoffee, newCookie, submitBug]);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([
newCoffee,
drinkCoffee,
newCookie,
submitBug,
]);
ds.view.setFilter(undefined);
expect(rawOutput(ds)).toEqual([newCoffee, drinkCoffee, newCookie, submitBug]);
ds.setSortBy(undefined);
ds.view.setSortBy(undefined);
// key insertion order
expect(unwrap(ds.output)).toEqual([
newCookie,
drinkCoffee,
submitBug,
newCoffee,
]);
expect(rawOutput(ds)).toEqual([newCookie, drinkCoffee, submitBug, newCoffee]);
});
test('filter + sort + index', () => {
@@ -259,18 +267,18 @@ test('filter + sort + index', () => {
key: 'id',
});
ds.setFilter((t) => t.title.indexOf('c') === -1);
ds.setSortBy('title');
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
ds.view.setSortBy('title');
expect(unwrap(ds.output)).toEqual([submitBug]);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([b, submitBug]);
expect(rawOutput(ds)).toEqual([b, submitBug]);
// filter in
const newCookie = {
@@ -278,7 +286,7 @@ test('filter + sort + index', () => {
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([b, newCookie, submitBug]);
expect(rawOutput(ds)).toEqual([b, newCookie, submitBug]);
// update -> filter in
const newCoffee = {
@@ -286,24 +294,24 @@ test('filter + sort + index', () => {
title: 'better drink tea',
};
ds.upsert(newCoffee);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie]);
ds.update(2, submitBug);
expect(unwrap(ds.output)).toEqual([newCoffee, b, newCookie, submitBug]);
expect(rawOutput(ds)).toEqual([newCoffee, b, newCookie, submitBug]);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([newCoffee, a, b, newCookie, submitBug]);
ds.view.setFilter(undefined);
expect(rawOutput(ds)).toEqual([newCoffee, a, b, newCookie, submitBug]);
ds.setSortBy(undefined);
ds.view.setSortBy(undefined);
// key insertion order
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, submitBug, a, b]);
// verify getOutput
expect(unwrap(ds.output.slice(1, 3))).toEqual([newCoffee, submitBug]);
expect(ds.getOutput(1, 3)).toEqual([newCoffee, submitBug]);
expect(rawOutput(ds).slice(1, 3)).toEqual([newCoffee, submitBug]);
expect(ds.view.output(1, 3)).toEqual([newCoffee, submitBug]);
});
test('filter', () => {
@@ -311,16 +319,16 @@ test('filter', () => {
key: 'id',
});
ds.setFilter((t) => t.title.indexOf('c') === -1);
expect(unwrap(ds.output)).toEqual([submitBug]);
ds.view.setFilter((t) => t.title.indexOf('c') === -1);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([submitBug]);
ds.append(b);
expect(unwrap(ds.output)).toEqual([submitBug, b]);
expect(rawOutput(ds)).toEqual([submitBug, b]);
// filter in
const newCookie = {
@@ -328,7 +336,7 @@ test('filter', () => {
title: 'eat a ookie',
};
ds.update(0, newCookie);
expect(unwrap(ds.output)).toEqual([newCookie, submitBug, b]);
expect(rawOutput(ds)).toEqual([newCookie, submitBug, b]);
// update -> filter in
const newCoffee = {
@@ -336,48 +344,48 @@ test('filter', () => {
title: 'better drink tea',
};
ds.upsert(newCoffee);
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, b]);
expect(rawOutput(ds)).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]);
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, b]);
ds.update(2, submitBug);
ds.setFilter(undefined);
expect(unwrap(ds.output)).toEqual([newCookie, newCoffee, submitBug, a, b]);
ds.view.setFilter(undefined);
expect(rawOutput(ds)).toEqual([newCookie, newCoffee, submitBug, a, b]);
});
test('reverse without sorting', () => {
const ds = createDataSource<Todo>([eatCookie, drinkCoffee]);
ds.setWindow(0, 100);
expect(ds.getOutput()).toEqual([eatCookie, drinkCoffee]);
ds.view.setWindow(0, 100);
expect(ds.view.output()).toEqual([eatCookie, drinkCoffee]);
ds.toggleReversed();
expect(ds.getOutput(1, 2)).toEqual([eatCookie]);
expect(ds.getOutput(0, 1)).toEqual([drinkCoffee]);
expect(ds.getOutput(0, 2)).toEqual([drinkCoffee, eatCookie]);
ds.view.toggleReversed();
expect(ds.view.output(1, 2)).toEqual([eatCookie]);
expect(ds.view.output(0, 1)).toEqual([drinkCoffee]);
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);
expect(ds.records).toEqual([eatCookie, drinkCoffee, submitBug]);
expect(ds.getOutput()).toEqual([submitBug, drinkCoffee, eatCookie]);
expect(ds.records()).toEqual([eatCookie, drinkCoffee, submitBug]);
expect(ds.view.output()).toEqual([submitBug, drinkCoffee, eatCookie]);
const x = {id: 'x', title: 'x'};
ds.update(0, x);
expect(ds.records).toEqual([x, drinkCoffee, submitBug]);
expect(ds.getOutput()).toEqual([submitBug, drinkCoffee, x]);
expect(ds.records()).toEqual([x, drinkCoffee, submitBug]);
expect(ds.view.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.getOutput()).toEqual([z, y, x]);
expect(ds.records()).toEqual([x, y, z]);
expect(ds.view.output()).toEqual([z, y, x]);
ds.setReversed(false);
expect(ds.getOutput()).toEqual([x, y, z]);
ds.view.setReversed(false);
expect(ds.view.output()).toEqual([x, y, z]);
});
test('reverse with sorting', () => {
@@ -393,23 +401,23 @@ test('reverse with sorting', () => {
const c = {$: 'c', name: 'c'};
const ds = createDataSource<N>([]);
ds.setWindow(0, 100);
ds.setReversed(true);
ds.view.setWindow(0, 100);
ds.view.setReversed(true);
ds.append(b1);
ds.append(c);
expect(ds.getOutput()).toEqual([c, b1]);
expect(ds.view.output()).toEqual([c, b1]);
ds.setSortBy('$');
expect(ds.getOutput()).toEqual([c, b1]);
ds.view.setSortBy('$');
expect(ds.view.output()).toEqual([c, b1]);
ds.append(b2);
expect(ds.getOutput()).toEqual([c, b2, b1]);
expect(ds.view.output()).toEqual([c, b2, b1]);
ds.append(a);
expect(ds.getOutput()).toEqual([c, b2, b1, a]);
expect(ds.view.output()).toEqual([c, b2, b1, a]);
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
const b4 = {
@@ -417,7 +425,7 @@ test('reverse with sorting', () => {
name: '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
const b2r = {
@@ -425,7 +433,7 @@ test('reverse with sorting', () => {
name: 'b2replacement',
};
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
const b3r = {
@@ -433,45 +441,45 @@ test('reverse with sorting', () => {
name: 'b3replacement',
};
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);
expect(ds.getOutput()).toEqual([c, b4, b2r, b1, a]);
ds.delete(4);
expect(ds.view.output()).toEqual([c, b4, b2r, b1, a]);
});
test('reset', () => {
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
key: 'id',
});
ds.setSortBy('title');
ds.setFilter((v) => v.id !== 'cookie');
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.view.setSortBy('title');
ds.view.setFilter((v) => v.id !== 'cookie');
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.reset();
expect(unwrap(ds.output)).toEqual([submitBug, drinkCoffee, eatCookie]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.view.reset();
expect(rawOutput(ds)).toEqual([submitBug, drinkCoffee, eatCookie]);
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
});
test('clear', () => {
const ds = createDataSource<Todo>([submitBug, drinkCoffee, eatCookie], {
key: 'id',
});
ds.setSortBy('title');
ds.setFilter((v) => v.id !== 'cookie');
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
expect([...ds.recordsById.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.view.setSortBy('title');
ds.view.setFilter((v) => v.id !== 'cookie');
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
expect([...ds.keys()]).toEqual(['bug', 'coffee', 'cookie']);
ds.clear();
expect(unwrap(ds.output)).toEqual([]);
expect([...ds.recordsById.keys()]).toEqual([]);
expect(rawOutput(ds)).toEqual([]);
expect([...ds.keys()]).toEqual([]);
ds.append(eatCookie);
ds.append(drinkCoffee);
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
expect(unwrap(ds.output)).toEqual([drinkCoffee, submitBug]);
expect(rawOutput(ds)).toEqual([drinkCoffee, submitBug]);
});
function testEvents<T>(
@@ -481,9 +489,9 @@ function testEvents<T>(
): any[] {
const ds = createDataSource<T>(initial, {key});
const events: any[] = [];
ds.setOutputChangeListener((e) => events.push(e));
ds.view.setListener((e) => events.push(e));
op(ds);
ds.setOutputChangeListener(undefined);
ds.view.setListener(undefined);
return events;
}
@@ -507,7 +515,7 @@ test('it emits the right events - zero window', () => {
test('it emits the right events - small window', () => {
expect(
testEvents(['a', 'b'], (ds) => {
ds.setWindow(0, 3);
ds.view.setWindow(0, 3);
ds.append('c');
ds.update(1, 'x');
}),
@@ -520,13 +528,13 @@ test('it emits the right events - small window', () => {
test('it emits the right events - view change', () => {
expect(
testEvents(['a', 'b'], (ds) => {
ds.setWindow(1, 2);
ds.setSortBy((x) => x);
ds.view.setWindow(1, 2);
ds.view.setSortBy((x) => x);
// a, [b]
ds.update(0, 'x');
// b, [x]
expect(ds.getItem(0)).toEqual('b');
expect(ds.getItem(1)).toEqual('x');
expect(ds.view.get(0)).toEqual('b');
expect(ds.view.get(1)).toEqual('x');
ds.append('y');
// b, [x], y
ds.append('c');
@@ -544,14 +552,14 @@ test('it emits the right events - view change', () => {
test('it emits the right events - reversed view change', () => {
expect(
testEvents(['a', 'b'], (ds) => {
ds.setWindow(1, 2);
ds.setSortBy((x) => x);
ds.setReversed(true);
ds.view.setWindow(1, 2);
ds.view.setSortBy((x) => x);
ds.view.setReversed(true);
// b, [a]
ds.update(0, 'x');
// x, [b]
expect(ds.getItem(0)).toEqual('x');
expect(ds.getItem(1)).toEqual('b');
expect(ds.view.get(0)).toEqual('x');
expect(ds.view.get(1)).toEqual('b');
ds.append('y');
// y, [x], b
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', () => {
expect(
testEvents(['a', 'b'], (ds) => {
ds.setWindow(0, 2);
ds.setSortBy((x) => x);
ds.setReversed(true);
ds.setFilter((x) => ['a', 'b'].includes(x));
ds.view.setWindow(0, 2);
ds.view.setSortBy((x) => x);
ds.view.setReversed(true);
ds.view.setFilter((x) => ['a', 'b'].includes(x));
// [b, a]
ds.update(0, 'x'); // x b
// [b, ]
expect(ds.getItem(0)).toEqual('b');
expect(ds.output.length).toBe(1);
expect(ds.view.get(0)).toEqual('b');
expect(rawOutput(ds).length).toBe(1);
ds.append('y'); // x b y
// [b, ]
ds.append('c'); // x b y c
@@ -590,9 +598,9 @@ test('it emits the right events - reversed view change with filter', () => {
// [b, a]
ds.append('a'); // x b y c a a
// [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
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]
}),
).toEqual([
@@ -612,16 +620,16 @@ test('basic remove', () => {
testEvents(
[drinkCoffee, eatCookie, submitBug],
(ds) => {
ds.setWindow(0, 100);
ds.remove(0);
expect(ds.getOutput()).toEqual([eatCookie, submitBug]);
expect(ds.recordsById.get('bug')).toBe(submitBug);
expect(ds.recordsById.get('coffee')).toBeUndefined();
expect(ds.recordsById.get('cookie')).toBe(eatCookie);
ds.view.setWindow(0, 100);
ds.delete(0);
expect(ds.view.output()).toEqual([eatCookie, submitBug]);
expect(ds.getById('bug')).toBe(submitBug);
expect(ds.getById('coffee')).toBeUndefined();
expect(ds.getById('cookie')).toBe(eatCookie);
ds.upsert(completedBug);
ds.removeByKey('cookie');
expect(ds.getOutput()).toEqual([completedBug]);
expect(ds.recordsById.get('bug')).toBe(completedBug);
ds.deleteByKey('cookie');
expect(ds.view.output()).toEqual([completedBug]);
expect(ds.getById('bug')).toBe(completedBug);
},
'id',
),
@@ -653,16 +661,16 @@ test('basic shift', () => {
testEvents(
[drinkCoffee, eatCookie, submitBug],
(ds) => {
ds.setWindow(0, 100);
ds.view.setWindow(0, 100);
ds.shift(2);
expect(ds.getOutput()).toEqual([submitBug]);
expect(ds.recordsById.get('bug')).toBe(submitBug);
expect(ds.recordsById.get('coffee')).toBeUndefined();
expect(ds.indexOfKey('bug')).toBe(0);
expect(ds.indexOfKey('coffee')).toBe(-1);
expect(ds.view.output()).toEqual([submitBug]);
expect(ds.getById('bug')).toBe(submitBug);
expect(ds.getById('coffee')).toBeUndefined();
expect(ds.getIndexOfKey('bug')).toBe(0);
expect(ds.getIndexOfKey('coffee')).toBe(-1);
ds.upsert(completedBug);
expect(ds.getOutput()).toEqual([completedBug]);
expect(ds.recordsById.get('bug')).toBe(completedBug);
expect(ds.view.output()).toEqual([completedBug]);
expect(ds.getById('bug')).toBe(completedBug);
},
'id',
),
@@ -684,11 +692,11 @@ test('basic shift', () => {
test('sorted shift', () => {
expect(
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
ds.setWindow(0, 100);
ds.setSortBy((v) => v);
expect(ds.getOutput()).toEqual(['a', 'b', 'c', 'd', 'e']);
ds.view.setWindow(0, 100);
ds.view.setSortBy((v) => v);
expect(ds.view.output()).toEqual(['a', 'b', 'c', 'd', 'e']);
ds.shift(4);
expect(ds.getOutput()).toEqual(['d']);
expect(ds.view.output()).toEqual(['d']);
ds.shift(1); // optimizes to reset
}),
).toEqual([
@@ -704,11 +712,11 @@ test('sorted shift', () => {
test('filtered shift', () => {
expect(
testEvents(['c', 'b', 'a', 'e', 'd'], (ds) => {
ds.setWindow(0, 100);
ds.setFilter((v) => v !== 'b' && v !== 'e');
expect(ds.getOutput()).toEqual(['c', 'a', 'd']);
ds.view.setWindow(0, 100);
ds.view.setFilter((v) => v !== 'b' && v !== 'e');
expect(ds.view.output()).toEqual(['c', 'a', 'd']);
ds.shift(4);
expect(ds.getOutput()).toEqual(['d']);
expect(ds.view.output()).toEqual(['d']);
}),
).toEqual([
{newCount: 3, type: 'reset'}, // filter
@@ -724,16 +732,16 @@ test('remove after shift works correctly', () => {
testEvents(
[eatCookie, drinkCoffee, submitBug, a, b],
(ds) => {
ds.setWindow(0, 100);
ds.view.setWindow(0, 100);
ds.shift(2);
ds.removeByKey('b');
ds.removeByKey('bug');
expect(ds.getOutput()).toEqual([a]);
expect(ds.indexOfKey('cookie')).toBe(-1);
expect(ds.indexOfKey('coffee')).toBe(-1);
expect(ds.indexOfKey('bug')).toBe(-1);
expect(ds.indexOfKey('a')).toBe(0);
expect(ds.indexOfKey('b')).toBe(-1);
ds.deleteByKey('b');
ds.deleteByKey('bug');
expect(ds.view.output()).toEqual([a]);
expect(ds.getIndexOfKey('cookie')).toBe(-1);
expect(ds.getIndexOfKey('coffee')).toBe(-1);
expect(ds.getIndexOfKey('bug')).toBe(-1);
expect(ds.getIndexOfKey('a')).toBe(0);
expect(ds.getIndexOfKey('b')).toBe(-1);
},
'id',
),
@@ -764,7 +772,7 @@ test('remove after shift works correctly', () => {
test('respects limit', () => {
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]];
};
@@ -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],
{limit: 20},
);
ds.setWindow(0, 100);
ds.view.setWindow(0, 100);
ds.append(19);
ds.append(20);
@@ -783,7 +791,7 @@ test('respects limit', () => {
ds.append(22);
expect(grab()).toEqual([20, 3, 22]);
ds.remove(0);
ds.delete(0);
expect(grab()).toEqual([19, 4, 22]);
ds.append(23);
@@ -791,3 +799,51 @@ test('respects limit', () => {
ds.append(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([]);
});