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>[];
amount: number;
};
type SINewIndexValueEvent<T> = {
type: 'siNewIndexValue';
indexKey: string;
value: T;
firstOfKind: boolean;
};
type ClearEvent = {
type: 'clear';
};
type DataEvent<T> =
| AppendEvent<T>
| UpdateEvent<T>
| RemoveEvent<T>
| ShiftEvent<T>;
| ShiftEvent<T>
| SINewIndexValueEvent<T>
| ClearEvent;
type Entry<T> = {
value: T;
@@ -181,7 +192,7 @@ export class DataSource<T extends any, KeyType = never> {
[viewId: string]: DataSourceView<T, KeyType>;
};
public readonly outputEventEmitter = new EventEmitter();
private readonly outputEventEmitter = new EventEmitter();
constructor(
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 -1 if the record wansn't found
@@ -469,6 +484,7 @@ export class DataSource<T extends any, KeyType = never> {
this.shiftOffset = 0;
this.idToIndex.clear();
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() {
if (!this.keyAttribute) {
throw new Error(
@@ -571,6 +597,12 @@ export class DataSource<T extends any, KeyType = never> {
} else {
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];
}
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(
record: T,
// assumes keys is already ordered
keys: IndexDefinition<T>,
): any {
): string {
return JSON.stringify(
Object.fromEntries(keys.map((k) => [k, String(record[k])])),
);
@@ -993,6 +1035,10 @@ export class DataSourceView<T, KeyType> {
}
break;
}
case 'clear':
case 'siNewIndexValue': {
break;
}
default:
throw new Error('unknown event type');
}

View File

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