Support tracking secondary indecies in DataSource

Reviewed By: LukeDefeo

Differential Revision: D51026559

fbshipit-source-id: 1f8f40ceedf70dfdc8978e0d6e447a1a58f8f82a
This commit is contained in:
Andrey Goncharov
2023-11-08 02:08:25 -08:00
committed by Facebook GitHub Bot
parent da5856138d
commit 701ae01501
2 changed files with 124 additions and 3 deletions

View File

@@ -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');
} }

View File

@@ -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,
});
}); });