diff --git a/desktop/flipper-plugin-core/src/data-source/DataSource.tsx b/desktop/flipper-plugin-core/src/data-source/DataSource.tsx index 3dd7e94bb..cb7dc2041 100644 --- a/desktop/flipper-plugin-core/src/data-source/DataSource.tsx +++ b/desktop/flipper-plugin-core/src/data-source/DataSource.tsx @@ -46,12 +46,23 @@ type ShiftEvent = { entries: Entry[]; amount: number; }; +type SINewIndexValueEvent = { + type: 'siNewIndexValue'; + indexKey: string; + value: T; + firstOfKind: boolean; +}; +type ClearEvent = { + type: 'clear'; +}; type DataEvent = | AppendEvent | UpdateEvent | RemoveEvent - | ShiftEvent; + | ShiftEvent + | SINewIndexValueEvent + | ClearEvent; type Entry = { value: T; @@ -181,7 +192,7 @@ export class DataSource { [viewId: string]: DataSourceView; }; - public readonly outputEventEmitter = new EventEmitter(); + private readonly outputEventEmitter = new EventEmitter(); constructor( keyAttribute: keyof T | undefined, @@ -262,6 +273,10 @@ export class DataSource { }; } + public secondaryIndicesKeys(): string[] { + return [...this._secondaryIndices.keys()]; + } + /** * Returns the index of a specific key in the *records* set. * Returns -1 if the record wansn't found @@ -469,6 +484,7 @@ export class DataSource { this.shiftOffset = 0; this.idToIndex.clear(); this.rebuild(); + this.emitDataEvent({type: 'clear'}); } /** @@ -522,6 +538,16 @@ export class DataSource { } } + public addDataListener['type']>( + event: E, + cb: (data: Extract, {type: E}>) => void, + ) { + this.outputEventEmitter.addListener(event, cb); + return () => { + this.outputEventEmitter.removeListener(event, cb); + }; + } + private assertKeySet() { if (!this.keyAttribute) { throw new Error( @@ -571,6 +597,12 @@ export class DataSource { } else { a.push(value); } + this.emitDataEvent({ + type: 'siNewIndexValue', + indexKey: indexValue, + value, + firstOfKind: !a, + }); } } @@ -631,11 +663,21 @@ export class DataSource { return this.getAllRecordsByIndex(indexQuery)[0]; } + public getAllIndexValues(index: IndexDefinition) { + const sortedKeys = index.slice().sort(); + const indexKey = sortedKeys.join(':'); + const recordsByIndex = this._recordsBySecondaryIndex.get(indexKey); + if (!recordsByIndex) { + return; + } + return [...recordsByIndex.keys()]; + } + private getSecondaryIndexValueFromRecord( record: T, // assumes keys is already ordered keys: IndexDefinition, - ): any { + ): string { return JSON.stringify( Object.fromEntries(keys.map((k) => [k, String(record[k])])), ); @@ -993,6 +1035,10 @@ export class DataSourceView { } break; } + case 'clear': + case 'siNewIndexValue': { + break; + } default: throw new Error('unknown event type'); } diff --git a/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx b/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx index 20088bf10..0fb0a4705 100644 --- a/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx +++ b/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx @@ -912,6 +912,13 @@ test('secondary keys - lookup by single key', () => { 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( ds.getAllRecordsByIndex({ title: 'eat a cookie', @@ -938,6 +945,12 @@ test('secondary keys - lookup by single key', () => { }), ).toEqual(submitBug); + expect(ds.getAllIndexValues(['id'])).toEqual([ + JSON.stringify({id: 'cookie'}), + JSON.stringify({id: 'coffee'}), + JSON.stringify({id: 'bug'}), + ]); + ds.delete(0); // eat Cookie expect( ds.getAllRecordsByIndex({ @@ -945,6 +958,13 @@ test('secondary keys - lookup by single key', () => { }), ).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 const n = { id: 'bug', @@ -972,6 +992,12 @@ test('secondary keys - lookup by single key', () => { title: 'eat a cookie', }), ).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', () => { @@ -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( ds.getAllRecordsByIndex({ id: 'cookie', @@ -1014,6 +1047,13 @@ test('secondary keys - lookup by combined keys', () => { }), ).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 = { id: 'cookie', title: 'eat a cookie', @@ -1041,6 +1081,16 @@ test('secondary keys - lookup by combined keys', () => { }), ).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(); expect( ds.getAllRecordsByIndex({ @@ -1049,6 +1099,12 @@ test('secondary keys - lookup by combined keys', () => { }), ).toEqual([]); + expect(ds.getAllIndexValues(['id', 'title'])).toEqual([]); + expect(clearSub).toBeCalledTimes(1); + + const newIndexValueSub = jest.fn(); + ds.addDataListener('siNewIndexValue', newIndexValueSub); + ds.append(cookie2); expect( ds.getAllRecordsByIndex({ @@ -1056,4 +1112,23 @@ test('secondary keys - lookup by combined keys', () => { title: 'eat a cookie', }), ).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, + }); });