Support tracking secondary indecies in DataSource
Reviewed By: LukeDefeo Differential Revision: D51026559 fbshipit-source-id: 1f8f40ceedf70dfdc8978e0d6e447a1a58f8f82a
This commit is contained in:
committed by
Facebook GitHub Bot
parent
da5856138d
commit
701ae01501
@@ -46,12 +46,23 @@ type ShiftEvent<T> = {
|
|||||||
entries: Entry<T>[];
|
entries: Entry<T>[];
|
||||||
amount: number;
|
amount: number;
|
||||||
};
|
};
|
||||||
|
type SINewIndexValueEvent<T> = {
|
||||||
|
type: 'siNewIndexValue';
|
||||||
|
indexKey: string;
|
||||||
|
value: T;
|
||||||
|
firstOfKind: boolean;
|
||||||
|
};
|
||||||
|
type ClearEvent = {
|
||||||
|
type: 'clear';
|
||||||
|
};
|
||||||
|
|
||||||
type DataEvent<T> =
|
type DataEvent<T> =
|
||||||
| AppendEvent<T>
|
| AppendEvent<T>
|
||||||
| UpdateEvent<T>
|
| UpdateEvent<T>
|
||||||
| RemoveEvent<T>
|
| RemoveEvent<T>
|
||||||
| ShiftEvent<T>;
|
| ShiftEvent<T>
|
||||||
|
| SINewIndexValueEvent<T>
|
||||||
|
| ClearEvent;
|
||||||
|
|
||||||
type Entry<T> = {
|
type Entry<T> = {
|
||||||
value: T;
|
value: T;
|
||||||
@@ -181,7 +192,7 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
[viewId: string]: DataSourceView<T, KeyType>;
|
[viewId: string]: DataSourceView<T, KeyType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
public readonly outputEventEmitter = new EventEmitter();
|
private readonly outputEventEmitter = new EventEmitter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
keyAttribute: keyof T | undefined,
|
keyAttribute: keyof T | undefined,
|
||||||
@@ -262,6 +273,10 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public secondaryIndicesKeys(): string[] {
|
||||||
|
return [...this._secondaryIndices.keys()];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of a specific key in the *records* set.
|
* Returns the index of a specific key in the *records* set.
|
||||||
* Returns -1 if the record wansn't found
|
* Returns -1 if the record wansn't found
|
||||||
@@ -469,6 +484,7 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
this.shiftOffset = 0;
|
this.shiftOffset = 0;
|
||||||
this.idToIndex.clear();
|
this.idToIndex.clear();
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
|
this.emitDataEvent({type: 'clear'});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -522,6 +538,16 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addDataListener<E extends DataEvent<T>['type']>(
|
||||||
|
event: E,
|
||||||
|
cb: (data: Extract<DataEvent<T>, {type: E}>) => void,
|
||||||
|
) {
|
||||||
|
this.outputEventEmitter.addListener(event, cb);
|
||||||
|
return () => {
|
||||||
|
this.outputEventEmitter.removeListener(event, cb);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private assertKeySet() {
|
private assertKeySet() {
|
||||||
if (!this.keyAttribute) {
|
if (!this.keyAttribute) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -571,6 +597,12 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
} else {
|
} else {
|
||||||
a.push(value);
|
a.push(value);
|
||||||
}
|
}
|
||||||
|
this.emitDataEvent({
|
||||||
|
type: 'siNewIndexValue',
|
||||||
|
indexKey: indexValue,
|
||||||
|
value,
|
||||||
|
firstOfKind: !a,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,11 +663,21 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
return this.getAllRecordsByIndex(indexQuery)[0];
|
return this.getAllRecordsByIndex(indexQuery)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAllIndexValues(index: IndexDefinition<T>) {
|
||||||
|
const sortedKeys = index.slice().sort();
|
||||||
|
const indexKey = sortedKeys.join(':');
|
||||||
|
const recordsByIndex = this._recordsBySecondaryIndex.get(indexKey);
|
||||||
|
if (!recordsByIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return [...recordsByIndex.keys()];
|
||||||
|
}
|
||||||
|
|
||||||
private getSecondaryIndexValueFromRecord(
|
private getSecondaryIndexValueFromRecord(
|
||||||
record: T,
|
record: T,
|
||||||
// assumes keys is already ordered
|
// assumes keys is already ordered
|
||||||
keys: IndexDefinition<T>,
|
keys: IndexDefinition<T>,
|
||||||
): any {
|
): string {
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
Object.fromEntries(keys.map((k) => [k, String(record[k])])),
|
Object.fromEntries(keys.map((k) => [k, String(record[k])])),
|
||||||
);
|
);
|
||||||
@@ -993,6 +1035,10 @@ export class DataSourceView<T, KeyType> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'clear':
|
||||||
|
case 'siNewIndexValue': {
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error('unknown event type');
|
throw new Error('unknown event type');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -912,6 +912,13 @@ test('secondary keys - lookup by single key', () => {
|
|||||||
indices: [['id'], ['title'], ['done']],
|
indices: [['id'], ['title'], ['done']],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(ds.secondaryIndicesKeys()).toEqual(['id', 'title', 'done']);
|
||||||
|
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee'}),
|
||||||
|
JSON.stringify({id: 'bug'}),
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
ds.getAllRecordsByIndex({
|
ds.getAllRecordsByIndex({
|
||||||
title: 'eat a cookie',
|
title: 'eat a cookie',
|
||||||
@@ -938,6 +945,12 @@ test('secondary keys - lookup by single key', () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual(submitBug);
|
).toEqual(submitBug);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee'}),
|
||||||
|
JSON.stringify({id: 'bug'}),
|
||||||
|
]);
|
||||||
|
|
||||||
ds.delete(0); // eat Cookie
|
ds.delete(0); // eat Cookie
|
||||||
expect(
|
expect(
|
||||||
ds.getAllRecordsByIndex({
|
ds.getAllRecordsByIndex({
|
||||||
@@ -945,6 +958,13 @@ test('secondary keys - lookup by single key', () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual([cookie2]);
|
).toEqual([cookie2]);
|
||||||
|
|
||||||
|
// We do not remove empty index values (for now)
|
||||||
|
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee'}),
|
||||||
|
JSON.stringify({id: 'bug'}),
|
||||||
|
]);
|
||||||
|
|
||||||
// replace submit Bug
|
// replace submit Bug
|
||||||
const n = {
|
const n = {
|
||||||
id: 'bug',
|
id: 'bug',
|
||||||
@@ -972,6 +992,12 @@ test('secondary keys - lookup by single key', () => {
|
|||||||
title: 'eat a cookie',
|
title: 'eat a cookie',
|
||||||
}),
|
}),
|
||||||
).toEqual([cookie2]);
|
).toEqual([cookie2]);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee'}),
|
||||||
|
JSON.stringify({id: 'bug'}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('secondary keys - lookup by combined keys', () => {
|
test('secondary keys - lookup by combined keys', () => {
|
||||||
@@ -983,6 +1009,13 @@ test('secondary keys - lookup by combined keys', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(ds.secondaryIndicesKeys()).toEqual(['id:title', 'done:title']);
|
||||||
|
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||||
|
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
ds.getAllRecordsByIndex({
|
ds.getAllRecordsByIndex({
|
||||||
id: 'cookie',
|
id: 'cookie',
|
||||||
@@ -1014,6 +1047,13 @@ test('secondary keys - lookup by combined keys', () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual([eatCookie, cookie2]);
|
).toEqual([eatCookie, cookie2]);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||||
|
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||||
|
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||||
|
]);
|
||||||
|
|
||||||
const upsertedCookie = {
|
const upsertedCookie = {
|
||||||
id: 'cookie',
|
id: 'cookie',
|
||||||
title: 'eat a cookie',
|
title: 'eat a cookie',
|
||||||
@@ -1041,6 +1081,16 @@ test('secondary keys - lookup by combined keys', () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual(undefined);
|
).toEqual(undefined);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||||
|
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||||
|
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||||
|
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const clearSub = jest.fn();
|
||||||
|
ds.addDataListener('clear', clearSub);
|
||||||
|
|
||||||
ds.clear();
|
ds.clear();
|
||||||
expect(
|
expect(
|
||||||
ds.getAllRecordsByIndex({
|
ds.getAllRecordsByIndex({
|
||||||
@@ -1049,6 +1099,12 @@ test('secondary keys - lookup by combined keys', () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([]);
|
||||||
|
expect(clearSub).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
const newIndexValueSub = jest.fn();
|
||||||
|
ds.addDataListener('siNewIndexValue', newIndexValueSub);
|
||||||
|
|
||||||
ds.append(cookie2);
|
ds.append(cookie2);
|
||||||
expect(
|
expect(
|
||||||
ds.getAllRecordsByIndex({
|
ds.getAllRecordsByIndex({
|
||||||
@@ -1056,4 +1112,23 @@ test('secondary keys - lookup by combined keys', () => {
|
|||||||
title: 'eat a cookie',
|
title: 'eat a cookie',
|
||||||
}),
|
}),
|
||||||
).toEqual([cookie2]);
|
).toEqual([cookie2]);
|
||||||
|
|
||||||
|
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||||
|
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Because we have 2 indecies
|
||||||
|
expect(newIndexValueSub).toBeCalledTimes(2);
|
||||||
|
expect(newIndexValueSub).toBeCalledWith({
|
||||||
|
type: 'siNewIndexValue',
|
||||||
|
indexKey: JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||||
|
firstOfKind: true,
|
||||||
|
value: cookie2,
|
||||||
|
});
|
||||||
|
expect(newIndexValueSub).toBeCalledWith({
|
||||||
|
type: 'siNewIndexValue',
|
||||||
|
indexKey: JSON.stringify({done: 'true', title: 'eat a cookie'}),
|
||||||
|
firstOfKind: true,
|
||||||
|
value: cookie2,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user