Browse Source

Add context cache to data sources

pull/6802/head
Artur Arseniev 2 months ago
parent
commit
5b4bf17f28
  1. 48
      packages/core/src/data_sources/index.ts
  2. 7
      packages/core/src/data_sources/model/DataRecord.ts
  3. 18
      packages/core/src/data_sources/model/DataRecords.ts
  4. 20
      packages/core/src/data_sources/model/DataSource.ts
  5. 4
      packages/core/src/data_sources/model/DataSources.ts
  6. 32
      packages/core/test/specs/data_sources/dynamic_values/styles.ts
  7. 28
      packages/core/test/specs/data_sources/index.ts

48
packages/core/src/data_sources/index.ts

@ -58,6 +58,7 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
number: NumberOperation,
string: StringOperation,
};
private contextCache?: ObjectAny;
destroy(): void {}
constructor(em: EditorModel) {
@ -114,7 +115,11 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
* const value = dsm.getValue('ds_id.record_id.propName', 'defaultValue');
*/
getValue(path: string | string[], defValue?: any, opts?: { context?: Record<string, any> }) {
return get(opts?.context || this.getContext(), path, defValue);
if (opts?.context) return get(opts.context, path, defValue);
const value = this.getValueFromDataSources(path);
return value === undefined ? defValue : value;
}
/**
@ -140,18 +145,15 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
}
getContext() {
return this.all.reduce((acc, ds) => {
acc[ds.id] = ds.records.reduce((accR, dr, i) => {
const dataRecord = dr;
if (!this.contextCache) {
this.contextCache = this.all.reduce((acc, ds) => {
acc[ds.id] = ds.getContext();
const attributes = { ...dataRecord.attributes };
delete attributes.__p;
accR[dataRecord.id || i] = attributes;
return accR;
return acc;
}, {} as ObjectAny);
return acc;
}, {} as ObjectAny);
}
return this.contextCache;
}
/**
@ -182,7 +184,7 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
const result: [DataSource?, DataRecord?, string?] = [];
const [dsId, drId, ...resPath] = stringToPath(path || '');
const dataSource = this.get(dsId);
const dataRecord = dataSource?.records.get(drId);
const dataRecord = drId !== undefined && dataSource?.getRecord(drId);
dataSource && result.push(dataSource);
if (dataRecord) {
@ -193,6 +195,28 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
return result;
}
invalidateContextCache = () => {
this.contextCache = undefined;
};
private getValueFromDataSources(path: string | string[]) {
const pathParts = Array.isArray(path) ? path : stringToPath(path || '');
if (!pathParts.length) return undefined;
const [dsId, drId, ...resPath] = pathParts;
const dataSource = this.get(dsId);
if (!dataSource) return undefined;
if (drId === undefined) return dataSource.getContext();
const dataRecord = dataSource.getRecord(drId);
if (!dataRecord) return undefined;
const recordContext = dataRecord.getContext();
return resPath.length ? get(recordContext, resPath) : recordContext;
}
/**
* Store data sources to a JSON object.
* @returns {Array} Stored data sources.

7
packages/core/src/data_sources/model/DataRecord.ts

@ -56,6 +56,13 @@ export default class DataRecord<T extends DataRecordProps = DataRecordProps> ext
return this.cl.indexOf(this);
}
getContext() {
const attributes = { ...this.attributes };
delete attributes.__p;
return attributes;
}
/**
* Handles changes to the record's attributes.
* This method triggers a change event for each property that has been altered.

18
packages/core/src/data_sources/model/DataRecords.ts

@ -10,6 +10,24 @@ export default class DataRecords<T extends DataRecordProps = DataRecordProps> ex
super(models, options);
this.dataSource = options.dataSource;
}
getRecord(id: string | number): DataRecord<T> | undefined {
return this.get(id) || this.getRecordByIndex(id);
}
isIndexKey(id: string | number) {
return !this.get(id) && !!this.getRecordByIndex(id);
}
private getRecordByIndex(id: string | number) {
const index = this.getIndex(id);
return index === undefined ? undefined : this.at(index);
}
private getIndex(id: string | number) {
const index = typeof id === 'number' ? id : Number(id);
return Number.isInteger(index) && `${index}` === `${id}` && index >= 0 ? index : undefined;
}
}
DataRecords.prototype.model = DataRecord;

20
packages/core/src/data_sources/model/DataSource.ts

@ -171,7 +171,11 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
* @name getRecord
*/
getRecord(id: string | number): DataRecord | undefined {
return this.records.get(id);
return this.records.getRecord(id);
}
isRecordIndex(id: string | number) {
return this.records.isIndexKey(id);
}
/**
@ -185,6 +189,16 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
return [...this.records.models].map((record) => this.getRecord(record.id)!);
}
getContext() {
return this.records.reduce(
(acc, dataRecord, index) => {
acc[dataRecord.id || index] = dataRecord.getContext();
return acc;
},
{} as Record<string, Partial<DRProps>>,
);
}
/**
* Retrieves all records from the data source with resolved relations based on the schema.
*/
@ -317,7 +331,9 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
}
private handleChanges(dataRecord: any, c: any, o: any) {
const { em } = this;
const options = o || c;
this.em.changesUp(options, { dataRecord, options });
em.DataSources.invalidateContextCache();
em.changesUp(options, { dataRecord, options });
}
}

4
packages/core/src/data_sources/model/DataSources.ts

@ -1,4 +1,4 @@
import { Collection } from '../../common';
import { Collection, collectionEvents } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { DataRecordProps, DataSourceProps } from '../types';
import DataSource from './DataSource';
@ -14,5 +14,7 @@ export default class DataSources extends Collection<DataSource> {
this.model = (props: DataSourceProps, opts = {}) => {
return new DataSource(props, { ...opts, em });
};
this.on(collectionEvents, () => em.DataSources.invalidateContextCache());
}
}

32
packages/core/test/specs/data_sources/dynamic_values/styles.ts

@ -158,6 +158,38 @@ describe('StyleDataVariable', () => {
expect(updatedStyle).toHaveProperty('color', 'blue');
});
test('component style binding ignores unrelated record add/remove churn', () => {
dsm.add({
id: 'style-churn',
records: [
{ id: 'bound-record', color: 'red' },
{ id: 'other-record', color: 'blue' },
],
});
const cmp = cmpRoot.append({
tagName: 'h1',
type: 'text',
content: 'Hello World',
style: {
color: {
type: DataVariableType,
defaultValue: 'black',
path: 'style-churn.bound-record.color',
},
},
})[0];
const addStyleSpy = jest.spyOn(cmp, 'addStyle');
const ds = dsm.get('style-churn');
ds.addRecord({ id: 'new-record', color: 'green' });
ds.removeRecord('other-record');
expect(addStyleSpy).not.toHaveBeenCalled();
expect(cmp.getStyle()).toHaveProperty('color', 'red');
});
describe('Component style manipulations', () => {
test('adding a new dynamic style with addStyle', () => {
dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] });

28
packages/core/test/specs/data_sources/index.ts

@ -82,6 +82,34 @@ describe('DataSourceManager', () => {
expect(dsm.getValue(`ds1.id4.metadata.roles`)).toEqual(roles);
expect(dsm.getValue(`ds1.id4.metadata.roles[1]`)).toEqual(roles[1]);
});
test('with index-based record path', () => {
dsm.add({
id: 'recordsByIndex',
records: [{ name: 'First item' }, { name: 'Second item' }] as any,
});
expect(dsm.getValue('recordsByIndex.0.name')).toBe('First item');
expect(dsm.getValue('recordsByIndex.1.name')).toBe('Second item');
});
});
describe('getContext', () => {
test('memoizes until data changes', () => {
const ds = addDataSource();
const contextA = dsm.getContext();
const contextB = dsm.getContext();
expect(contextA).toBe(contextB);
ds.getRecord('id1')?.set({ name: 'Name1 updated' });
const contextC = dsm.getContext();
expect(contextC).not.toBe(contextA);
expect(contextC.ds1.id1.name).toBe('Name1 updated');
});
});
describe('setValue', () => {

Loading…
Cancel
Save