From c89bc6e05ac00152807f4b113c07afe747515126 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Fri, 4 Feb 2022 15:52:03 -0800 Subject: [PATCH 1/9] fix: requestBuffer * remove legacy sitewise resource * remove requestInformations from initiateRequest signature * fix requests to only be made for uncached data --- .../testing/testing-ground/siteWiseQueries.ts | 4 +- .../testing/testing-ground/testing-ground.tsx | 2 +- .../data-module/IotAppKitDataModule.spec.ts | 343 +++++++++++------- .../src/data-module/IotAppKitDataModule.ts | 112 +++--- .../data-source-store/dataSourceStore.spec.ts | 51 ++- .../data-source-store/dataSourceStore.ts | 25 +- .../subscriptionStore.spec.ts | 1 + .../subscription-store/subscriptionStore.ts | 4 +- packages/core/src/data-module/types.d.ts | 8 +- .../time-series-data/data-source.spec.ts | 258 +++++++------ .../time-series-data/data-source.ts | 21 +- .../testing/mock-data-source/data-source.ts | 64 ++-- .../src/testing/mock-data-source/types.d.ts | 7 - 13 files changed, 487 insertions(+), 413 deletions(-) diff --git a/packages/components/src/testing/testing-ground/siteWiseQueries.ts b/packages/components/src/testing/testing-ground/siteWiseQueries.ts index 9cb96204e..c24f569df 100644 --- a/packages/components/src/testing/testing-ground/siteWiseQueries.ts +++ b/packages/components/src/testing/testing-ground/siteWiseQueries.ts @@ -37,8 +37,8 @@ export const AGGREGATED_DATA_QUERY = { { assetId: AGGREGATED_DATA_ASSET, properties: [ - { propertyId: AGGREGATED_DATA_PROPERTY }, - { propertyId: AGGREGATED_DATA_PROPERTY_2, resolution: '1m' }, + { propertyId: AGGREGATED_DATA_PROPERTY, resolution: '0' }, + { propertyId: AGGREGATED_DATA_PROPERTY_2 }, ], }, ], diff --git a/packages/components/src/testing/testing-ground/testing-ground.tsx b/packages/components/src/testing/testing-ground/testing-ground.tsx index 337759f35..b97306c97 100755 --- a/packages/components/src/testing/testing-ground/testing-ground.tsx +++ b/packages/components/src/testing/testing-ground/testing-ground.tsx @@ -143,7 +143,7 @@ export class TestingGround { appKitSession={this.appKitSession} queries={[AGGREGATED_DATA_QUERY]} viewport={this.viewport} - settings={{ resolution: this.resolution }} + settings={{ resolution: this.resolution, requestBuffer: 10, fetchAggregatedData: true }} /> diff --git a/packages/core/src/data-module/IotAppKitDataModule.spec.ts b/packages/core/src/data-module/IotAppKitDataModule.spec.ts index 4f541eb7b..c75b0fd41 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.spec.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.spec.ts @@ -1,11 +1,11 @@ import flushPromises from 'flush-promises'; -import { DATA_STREAM, DATA_STREAM_INFO, STRING_INFO_1 } from '../testing/__mocks__/mockWidgetProperties'; -import { DataSource, DataSourceRequest, DataStreamQuery, DataStream } from './types.d'; -import { DataPoint, DataStreamInfo } from '@synchro-charts/core'; +import { DATA_STREAM, DATA_STREAM_INFO, DATA_STREAM_2 } from '../testing/__mocks__/mockWidgetProperties'; +import { DataSource, DataStreamQuery, DataStream } from './types.d'; +import { DataPoint } from '@synchro-charts/core'; import { TimeSeriesDataRequest, TimeSeriesDataRequestSettings } from './data-cache/requestTypes'; import { DataStreamsStore, DataStreamStore } from './data-cache/types'; import * as caching from './data-cache/caching/caching'; -import { createSiteWiseLegacyDataSource } from '../testing/mock-data-source/data-source'; +import { createMockSiteWiseDataSource } from '../testing/mock-data-source/data-source'; import { HOUR_IN_MS, MINUTE_IN_MS, MONTH_IN_MS, SECOND_IN_MS } from '../common/time'; import { IotAppKitDataModule } from './IotAppKitDataModule'; @@ -14,7 +14,6 @@ import { SiteWiseDataStreamQuery } from '../iotsitewise/time-series-data/types'; import { toDataStreamId, toSiteWiseAssetProperty } from '../iotsitewise/time-series-data/util/dataStreamId'; import Mock = jest.Mock; -import { SiteWiseLegacyDataStreamQuery } from '../testing/mock-data-source'; const { EMPTY_CACHE } = caching; @@ -30,25 +29,6 @@ const DATA_STREAM_QUERY: SiteWiseDataStreamQuery = { ], }; -// A simple mock data source, which will always immediately return a successful response of your choosing. -const createMockSiteWiseDataSource = ( - dataStreams: DataStream[], - resolution: number = 0 -): DataSource => ({ - name: SITEWISE_DATA_SOURCE, - initiateRequest: jest.fn(({ onSuccess }: DataSourceRequest) => onSuccess(dataStreams)), - getRequestsFromQuery: ({ query }) => - query.assets - .map(({ assetId, properties }) => - properties.map(({ propertyId, refId }) => ({ - id: toDataStreamId({ assetId, propertyId }), - refId, - resolution, - })) - ) - .flat(), -}); - const CUSTOM_DATA_SOURCE = 'custom-source'; type CustomDataStreamQuery = DataStreamQuery & { @@ -75,7 +55,7 @@ afterAll(() => { it('subscribes to an empty set of queries', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [] }); dataModule.registerDataSource(dataSource); const onSuccess = jest.fn(); @@ -99,7 +79,7 @@ it('subscribes to an empty set of queries', async () => { describe('update subscription', () => { it('provides new data streams when subscription is updated', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); @@ -180,7 +160,7 @@ describe('initial request', () => { const dataModule = new IotAppKitDataModule(); const dataSource: DataSource = { - ...createMockSiteWiseDataSource([DATA_STREAM]), + ...createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }), initiateRequest: jest.fn(), }; @@ -209,7 +189,7 @@ describe('initial request', () => { const dataModule = new IotAppKitDataModule(); const dataSource: DataSource = { - ...createMockSiteWiseDataSource([DATA_STREAM]), + ...createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }), initiateRequest: jest.fn(), }; @@ -232,15 +212,19 @@ describe('initial request', () => { } as DataStreamStore), ]); - expect(dataSource.initiateRequest).toBeCalledWith(expect.objectContaining({ query: DATA_STREAM_QUERY }), [ - { id: DATA_STREAM.id, resolution: DATA_STREAM.resolution, start: START, end: END }, - ]); + expect(dataSource.initiateRequest).toBeCalledWith( + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { viewport: { start: START, end: END }, settings: { fetchFromStartToEnd: true } }, + viewport: { start: START, end: END }, + }) + ); }); }); it('subscribes to a single data stream', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const { propertyId, assetId } = toSiteWiseAssetProperty(DATA_STREAM.id); @@ -295,7 +279,7 @@ it('throws error when subscribing to a non-existent data source', () => { }); it('requests data from a custom data source', () => { - const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + const customSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); const { propertyId, assetId } = toSiteWiseAssetProperty(DATA_STREAM.id); const dataModule = new IotAppKitDataModule(); @@ -326,7 +310,7 @@ it('requests data from a custom data source', () => { it('subscribes to multiple data streams', () => { const onRequestData = jest.fn(); - const source = createSiteWiseLegacyDataSource(onRequestData); + const source = createMockSiteWiseDataSource({ onRequestData }); const request: TimeSeriesDataRequest = { viewport: { start: new Date(1999, 0, 0), end: new Date() }, @@ -334,7 +318,13 @@ it('subscribes to multiple data streams', () => { fetchFromStartToEnd: true, }, }; - const dataStreamInfos: DataStreamInfo[] = [STRING_INFO_1, DATA_STREAM_INFO]; + + const assets = [ + { + assetId: 'asset-1', + properties: [{ propertyId: 'prop-1' }, { propertyId: 'prop2' }], + }, + ]; const dataModule = new IotAppKitDataModule(); const onSuccess = jest.fn(); @@ -343,7 +333,7 @@ it('subscribes to multiple data streams', () => { const query = { source: source.name, - dataStreamInfos, + assets, }; dataModule.subscribeToDataStreams( { @@ -353,13 +343,25 @@ it('subscribes to multiple data streams', () => { onSuccess ); - expect(onRequestData).toHaveBeenNthCalledWith(1, expect.objectContaining({ dataStreamId: STRING_INFO_1.id })); - expect(onRequestData).toHaveBeenNthCalledWith(2, expect.objectContaining({ dataStreamId: DATA_STREAM_INFO.id })); + expect(onRequestData).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + assetId: assets[0].assetId, + propertyId: assets[0].properties[0].propertyId, + }) + ); + expect(onRequestData).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + assetId: assets[0].assetId, + propertyId: assets[0].properties[1].propertyId, + }) + ); }); it('subscribes to multiple queries on the same data source', () => { const onRequestData = jest.fn(); - const source = createSiteWiseLegacyDataSource(onRequestData); + const source = createMockSiteWiseDataSource({ onRequestData }); const request: TimeSeriesDataRequest = { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, @@ -373,11 +375,21 @@ it('subscribes to multiple queries on the same data source', () => { const queries = [ { source: source.name, - dataStreamInfos: [STRING_INFO_1], + assets: [ + { + assetId: 'asset-1', + properties: [{ propertyId: 'prop-1' }], + }, + ], }, { source: source.name, - dataStreamInfos: [DATA_STREAM_INFO], + assets: [ + { + assetId: 'asset-2', + properties: [{ propertyId: 'prop-2' }], + }, + ], }, ]; dataModule.subscribeToDataStreams( @@ -388,17 +400,39 @@ it('subscribes to multiple queries on the same data source', () => { onSuccess ); - expect(onRequestData).toHaveBeenNthCalledWith(1, expect.objectContaining({ dataStreamId: STRING_INFO_1.id })); - expect(onRequestData).toHaveBeenNthCalledWith(2, expect.objectContaining({ dataStreamId: DATA_STREAM_INFO.id })); + expect(onRequestData).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + assetId: queries[0].assets[0].assetId, + propertyId: queries[0].assets[0].properties[0].propertyId, + }) + ); + expect(onRequestData).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + assetId: queries[1].assets[0].assetId, + propertyId: queries[1].assets[0].properties[0].propertyId, + }) + ); expect(onSuccess).toHaveBeenCalledWith([ - expect.objectContaining({ id: STRING_INFO_1.id }), - expect.objectContaining({ id: DATA_STREAM_INFO.id }), + expect.objectContaining({ + id: toDataStreamId({ + assetId: queries[0].assets[0].assetId, + propertyId: queries[0].assets[0].properties[0].propertyId, + }), + }), + expect.objectContaining({ + id: toDataStreamId({ + assetId: queries[1].assets[0].assetId, + propertyId: queries[1].assets[0].properties[0].propertyId, + }), + }), ]); }); it('subscribes to multiple data sources', () => { - const source = createSiteWiseLegacyDataSource(jest.fn()); + const source = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM_2] }); const customSource = createCustomMockDataSource([DATA_STREAM]); const request: TimeSeriesDataRequest = { @@ -416,7 +450,12 @@ it('subscribes to multiple data sources', () => { const queries = [ { source: source.name, - dataStreamInfos: [STRING_INFO_1], + assets: [ + { + assetId: 'some-asset-id-2', + properties: [{ propertyId: 'some-property-id-2' }], + }, + ], }, { source: customSource.name, @@ -432,13 +471,13 @@ it('subscribes to multiple data sources', () => { ); expect(onSuccess).toHaveBeenCalledWith([ - expect.objectContaining({ id: STRING_INFO_1.id }), + expect.objectContaining({ id: DATA_STREAM_2.id }), expect.objectContaining({ id: customSourceAssetId }), ]); }); it('subscribes to multiple data streams on multiple data sources', () => { - const source = createSiteWiseLegacyDataSource(jest.fn()); + const source = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM_2, DATA_STREAM] }); const customSource = createCustomMockDataSource([]); const request: TimeSeriesDataRequest = { @@ -457,7 +496,16 @@ it('subscribes to multiple data streams on multiple data sources', () => { const queries = [ { source: source.name, - dataStreamInfos: [DATA_STREAM_INFO, STRING_INFO_1], + assets: [ + { + assetId: 'some-asset-id-2', + properties: [{ propertyId: 'some-property-id-2' }], + }, + { + assetId: 'some-asset-id', + properties: [{ propertyId: 'some-property-id' }], + }, + ], }, { source: customSource.name, @@ -473,8 +521,8 @@ it('subscribes to multiple data streams on multiple data sources', () => { ); expect(onSuccess).toHaveBeenCalledWith([ - expect.objectContaining({ id: DATA_STREAM_INFO.id }), - expect.objectContaining({ id: STRING_INFO_1.id }), + expect.objectContaining({ id: DATA_STREAM_2.id }), + expect.objectContaining({ id: DATA_STREAM.id }), expect.objectContaining({ id: customSourceAssetId_1 }), expect.objectContaining({ id: customSourceAssetId_2 }), ]); @@ -482,7 +530,7 @@ it('subscribes to multiple data streams on multiple data sources', () => { it('only requests latest value', () => { const onRequestData = jest.fn(); - const source = createSiteWiseLegacyDataSource(onRequestData); + const source = createMockSiteWiseDataSource({ onRequestData }); const LATEST_VALUE_REQUEST_SETTINGS: TimeSeriesDataRequestSettings = { fetchMostRecentBeforeEnd: true }; @@ -495,9 +543,14 @@ it('only requests latest value', () => { { queries: [ { - dataStreamInfos: [DATA_STREAM_INFO], source: source.name, - } as SiteWiseLegacyDataStreamQuery, + assets: [ + { + assetId: 'asset-1', + properties: [{ propertyId: 'prop-1' }], + }, + ], + }, ], request: { viewport: { start: new Date(2000, 0, 0), end: new Date(2001, 0, 0) }, @@ -550,7 +603,7 @@ describe('error handling', () => { }; it('provides a data stream which has an error associated with it on initial subscription', () => { - const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + const customSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); const dataModule = new IotAppKitDataModule({ initialDataCache: CACHE_WITH_ERROR }); const dataStreamCallback = jest.fn(); @@ -573,7 +626,7 @@ describe('error handling', () => { }); it('does not re-request a data stream with an error associated with it', async () => { - const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + const customSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); const dataModule = new IotAppKitDataModule({ initialDataCache: CACHE_WITH_ERROR }); const dataStreamCallback = jest.fn(); @@ -600,7 +653,7 @@ describe('error handling', () => { }); it('does request a data stream which has no error associated with it', () => { - const customSource = createMockSiteWiseDataSource([DATA_STREAM]); + const customSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); const dataModule = new IotAppKitDataModule({ initialDataCache: CACHE_WITHOUT_ERROR }); @@ -627,7 +680,7 @@ describe('error handling', () => { describe('caching', () => { it('does not request already cached data', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const START_1 = new Date(2000, 1, 0); @@ -655,7 +708,7 @@ describe('caching', () => { it('requests only uncached data', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); // Order of points through time: @@ -677,31 +730,47 @@ describe('caching', () => { dataStreamCallback ); + expect(dataSource.initiateRequest).toHaveBeenCalledTimes(1); + + expect(dataSource.initiateRequest).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { viewport: { start: START_1, end: END_1 }, settings: { fetchFromStartToEnd: true } }, + viewport: { start: START_1, end: END_1 }, + }) + ); + (dataSource.initiateRequest as Mock).mockClear(); update({ request: { viewport: { start: START_2, end: END_2 }, settings: { fetchFromStartToEnd: true } } }); await flushPromises(); - expect(dataSource.initiateRequest).toBeCalledWith(expect.any(Object), [ - { - id: DATA_STREAM.id, - resolution: DATA_STREAM.resolution, - start: START_2, - end: START_1, - }, - { - id: DATA_STREAM.id, - resolution: DATA_STREAM.resolution, - start: END_1, - end: END_2, - }, - ]); + expect(dataSource.initiateRequest).toHaveBeenCalledTimes(2); + + expect(dataSource.initiateRequest).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { viewport: { start: START_2, end: START_1 }, settings: { fetchFromStartToEnd: true } }, + viewport: { start: START_2, end: END_2 }, + }) + ); + + expect(dataSource.initiateRequest).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { viewport: { start: END_1, end: END_2 }, settings: { fetchFromStartToEnd: true } }, + viewport: { start: START_2, end: END_2 }, + }) + ); }); it('immediately request when subscribed to an entirely new time interval not previously requested', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const START_1 = new Date(2000, 1, 0); @@ -724,19 +793,18 @@ describe('caching', () => { request: { viewport: { start: START_2, end: END_2 } }, }); - expect(dataSource.initiateRequest).toBeCalledWith(expect.any(Object), [ - { - id: DATA_STREAM_INFO.id, - resolution: DATA_STREAM_INFO.resolution, - start: START_2, - end: END_2, - }, - ]); + expect(dataSource.initiateRequest).toBeCalledWith( + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { viewport: { start: START_2, end: END_2 } }, + viewport: { start: START_2, end: END_2 }, + }) + ); }); it('requests already cached data if the default TTL has expired', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const END = new Date(); @@ -758,15 +826,17 @@ describe('caching', () => { jest.advanceTimersByTime(MINUTE_IN_MS); - expect(dataSource.initiateRequest).toBeCalledWith(expect.any(Object), [ - { - id: DATA_STREAM_INFO.id, - resolution: DATA_STREAM_INFO.resolution, - // 1 minute time advancement invalidates 3 minutes of cache by default, which is 2 minutes from END_1 - start: new Date(END.getTime() - 2 * MINUTE_IN_MS), - end: END, - }, - ]); + expect(dataSource.initiateRequest).toBeCalledWith( + expect.objectContaining({ + query: DATA_STREAM_QUERY, + request: { + // 1 minute time advancement invalidates 3 minutes of cache by default, which is 2 minutes from END_1 + viewport: { start: new Date(END.getTime() - 2 * MINUTE_IN_MS), end: END }, + settings: { fetchFromStartToEnd: true, refreshRate: MINUTE_IN_MS }, + }, + viewport: { start: START, end: END }, + }) + ); }); it('requests already cached data if custom TTL has expired', async () => { @@ -778,7 +848,7 @@ describe('caching', () => { }; const dataModule = new IotAppKitDataModule({ cacheSettings: customCacheSettings }); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const END = new Date(); @@ -796,15 +866,17 @@ describe('caching', () => { (dataSource.initiateRequest as Mock).mockClear(); jest.advanceTimersByTime(MINUTE_IN_MS); - expect(dataSource.initiateRequest).toBeCalledWith(expect.any(Object), [ - { - id: DATA_STREAM_INFO.id, - resolution: DATA_STREAM_INFO.resolution, + expect(dataSource.initiateRequest).toBeCalledWith( + expect.objectContaining({ + query: DATA_STREAM_QUERY, // 1 minute time advancement invalidates 5 minutes of cache with custom mapping, which is 4 minutes from END_1 - start: new Date(END.getTime() - 4 * MINUTE_IN_MS), - end: END, - }, - ]); + request: { + viewport: { start: new Date(END.getTime() - 4 * MINUTE_IN_MS), end: END }, + settings: { refreshRate: MINUTE_IN_MS }, + }, + viewport: { start: START, end: END }, + }) + ); }); }); @@ -817,7 +889,7 @@ it('overrides module-level cache TTL if query-level cache TTL is provided', asyn }; const dataModule = new IotAppKitDataModule({ cacheSettings: customCacheSettings }); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const END = new Date(); @@ -845,21 +917,31 @@ it('overrides module-level cache TTL if query-level cache TTL is provided', asyn (dataSource.initiateRequest as Mock).mockClear(); jest.advanceTimersByTime(MINUTE_IN_MS); - expect(dataSource.initiateRequest).toBeCalledWith(expect.any(Object), [ - { - id: DATA_STREAM_INFO.id, - resolution: DATA_STREAM_INFO.resolution, + expect(dataSource.initiateRequest).toBeCalledWith( + expect.objectContaining({ + query: { + ...DATA_STREAM_QUERY, + cacheSettings: { + ttlDurationMapping: { + [MINUTE_IN_MS]: 0, + [10 * MINUTE_IN_MS]: 30 * SECOND_IN_MS, + }, + }, + }, // 1 minute time advancement invalidates 10 minutes of cache with query-level mapping, which is 9 minutes from END_1 - start: new Date(END.getTime() - 9 * MINUTE_IN_MS), - end: END, - }, - ]); + request: { + viewport: { start: new Date(END.getTime() - 9 * MINUTE_IN_MS), end: END }, + settings: { refreshRate: MINUTE_IN_MS }, + }, + viewport: { start: START, end: END }, + }) + ); }); describe('request scheduler', () => { it('periodically requests duration based queries', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -894,7 +976,7 @@ describe('request scheduler', () => { }; const dataModule = new IotAppKitDataModule({ cacheSettings: customCacheSettings }); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const END = new Date(); @@ -935,7 +1017,7 @@ describe('request scheduler', () => { it('stops requesting for data after unsubscribing', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -965,7 +1047,7 @@ describe('request scheduler', () => { it('periodically requests data after switching from static to duration based viewport', async () => { const DATA_POINT: DataPoint = { x: Date.now(), y: 1921 }; const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([{ ...DATA_STREAM, data: [DATA_POINT] }]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [{ ...DATA_STREAM, data: [DATA_POINT] }] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -1006,7 +1088,7 @@ describe('request scheduler', () => { it('stops the request scheduler when we first update request info to have duration and then call unsubscribe', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -1044,7 +1126,7 @@ describe('request scheduler', () => { it('stops the request scheduler when request info gets updated with static viewport that does not intersect with any TTL intervals', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -1074,7 +1156,7 @@ describe('request scheduler', () => { it('continues the schedule requests when request info gets updated with static viewport that intersects with TTL intervals', async () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -1105,7 +1187,7 @@ describe('request scheduler', () => { it('when data is requested from the viewport start to end with a buffer, include a buffer', () => { const dataModule = new IotAppKitDataModule(); - const dataSource = createMockSiteWiseDataSource([DATA_STREAM]); + const dataSource = createMockSiteWiseDataSource({ dataStreams: [DATA_STREAM] }); dataModule.registerDataSource(dataSource); const dataStreamCallback = jest.fn(); @@ -1118,25 +1200,24 @@ it('when data is requested from the viewport start to end with a buffer, include const { unsubscribe } = dataModule.subscribeToDataStreams( { queries: [DATA_STREAM_QUERY], - request: { viewport: { start, end }, settings: { requestBuffer, fetchFromStartToEnd: true } }, + request: { viewport: { start, end }, settings: { requestBuffer } }, }, dataStreamCallback ); expect(dataSource.initiateRequest).toBeCalledWith( expect.objectContaining({ - request: expect.objectContaining({ - settings: expect.objectContaining({ + request: { + settings: { requestBuffer, - }), - }), - }), - expect.arrayContaining([ - expect.objectContaining({ - start: expectedStart, - end: expectedEnd, - }), - ]) + }, + viewport: { + start: expectedStart, + end: expectedEnd, + }, + }, + viewport: { start, end }, + }) ); unsubscribe(); diff --git a/packages/core/src/data-module/IotAppKitDataModule.ts b/packages/core/src/data-module/IotAppKitDataModule.ts index d86c463db..e4e2a59ea 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.ts @@ -1,3 +1,4 @@ +import { MinimalViewPortConfig } from '@synchro-charts/core'; import { v4 } from 'uuid'; import SubscriptionStore from './subscription-store/subscriptionStore'; import { @@ -71,40 +72,30 @@ export class IotAppKitDataModule implements DataModule { * segments within the cache. */ private fulfillQueries = ({ - queries, - start, - end, + viewport, request, + queries, }: { - queries: DataStreamQuery[]; - start: Date; - end: Date; + viewport: MinimalViewPortConfig; request: TimeSeriesDataRequest; + queries: DataStreamQuery[]; }) => { - const requestedStreams = this.dataSourceStore.getRequestsFromQueries({ queries, request }); + const start = viewportStartDate(request.viewport); + const end = viewportEndDate(request.viewport); + + const requestedStreams = this.dataSourceStore.getRequestsFromQueries({ queries, request, viewport }); const isRequestedDataStream = ({ id, resolution }: RequestInformation) => this.dataCache.shouldRequestDataStream({ dataStreamId: id, resolution }); const requiredStreams = requestedStreams.filter(isRequestedDataStream); - // Get the date range to request data for. - // Pass in 'now' for max since we don't want to request for data in the future yet - it doesn't exist yet. - const { start: adjustedStart, end: adjustedEnd } = requestRange( - { - start, - end, - max: new Date(), - }, - request.settings?.requestBuffer - ); - const requests = requiredStreams .map(({ resolution, id, cacheSettings }) => { const dateRanges = getDateRangesToRequest({ store: this.dataCache.getState(), - start: adjustedStart, - end: adjustedEnd, + start, + end, resolution, dataStreamId: id, cacheSettings: { ...this.cacheSettings, ...cacheSettings }, @@ -120,35 +111,55 @@ export class IotAppKitDataModule implements DataModule { ); /** Indicate within the cache that the following queries are being requested */ - requests.forEach(({ start: reqStart, end: reqEnd, id, resolution }) => + requests.forEach(({ start: reqStart, end: reqEnd, id, resolution }) => { this.dataCache.onRequest({ id, resolution, first: reqStart, last: reqEnd, - }) + }); + + this.registerRequest({ queries, request: { ...request, viewport: { start: reqStart, end: reqEnd } }, viewport }); + }); + }; + + private getAdjustedRequest = (request: TimeSeriesDataRequest): TimeSeriesDataRequest => { + // Get the date range to request data for. + // Pass in 'now' for max since we don't want to request for data in the future yet - it doesn't exist yet. + const { start, end } = requestRange( + { + start: viewportStartDate(request.viewport), + end: viewportEndDate(request.viewport), + max: new Date(), + }, + request.settings?.requestBuffer ); - if (requests.length > 0) { - this.registerRequest({ queries, request }, requests); - } + return { ...request, viewport: { start, end } }; }; public subscribeToDataStreams = ( - { queries, request }: DataModuleSubscription, + subscription: DataModuleSubscription, callback: DataStreamCallback ): SubscriptionResponse => { const subscriptionId = v4(); + const request = this.getAdjustedRequest(subscription.request); + + const viewport = { + start: viewportStartDate(subscription.request.viewport), + end: viewportEndDate(subscription.request.viewport), + }; + this.subscriptions.addSubscription(subscriptionId, { - queries, + ...subscription, request, + viewport, emit: callback, fulfill: () => { this.fulfillQueries({ - start: viewportStartDate(request.viewport), - end: viewportEndDate(request.viewport), - queries, + viewport, + queries: subscription.queries, request, }); }, @@ -177,37 +188,44 @@ export class IotAppKitDataModule implements DataModule { const updatedSubscription = Object.assign({}, subscription, subscriptionUpdate) as Subscription; + const request = this.getAdjustedRequest(updatedSubscription.request); + + const viewport = { + start: viewportStartDate(updatedSubscription.request.viewport), + end: viewportEndDate(updatedSubscription.request.viewport), + }; + if ('queries' in updatedSubscription) { this.subscriptions.updateSubscription(subscriptionId, { ...updatedSubscription, + request, + viewport, fulfill: () => { this.fulfillQueries({ - start: viewportStartDate(updatedSubscription.request.viewport), - end: viewportEndDate(updatedSubscription.request.viewport), + viewport, queries: updatedSubscription.queries, - request: updatedSubscription.request, + request, }); }, }); } }; - private registerRequest = ( - subscription: { queries: Query[]; request: TimeSeriesDataRequest }, - requestInformations: RequestInformationAndRange[] - ): void => { - const { queries, request } = subscription; + private registerRequest = (subscription: { + queries: Query[]; + request: TimeSeriesDataRequest; + viewport: MinimalViewPortConfig; + }): void => { + const { queries, request, viewport } = subscription; queries.forEach((query) => - this.dataSourceStore.initiateRequest( - { - request, - query, - onSuccess: this.dataCache.onSuccess(request), - onError: this.dataCache.onError, - }, - requestInformations - ) + this.dataSourceStore.initiateRequest({ + request, + query, + viewport, + onSuccess: this.dataCache.onSuccess(request), + onError: this.dataCache.onError, + }) ); }; diff --git a/packages/core/src/data-module/data-source-store/dataSourceStore.spec.ts b/packages/core/src/data-module/data-source-store/dataSourceStore.spec.ts index 1c356f0e4..7178cdcdd 100644 --- a/packages/core/src/data-module/data-source-store/dataSourceStore.spec.ts +++ b/packages/core/src/data-module/data-source-store/dataSourceStore.spec.ts @@ -17,25 +17,24 @@ it('initiate a request on a registered data source', () => { const query = { source: 'custom' }; const request = { viewport: { start: new Date(), end: new Date() }, settings: { fetchFromStartToEnd: true } }; - dataSourceStore.initiateRequest( - { - request, - query, - onSuccess: () => {}, - onError: () => {}, - }, - [] - ); - expect(customSource.initiateRequest).toBeCalledWith( - { - request, - query, - onSuccess: expect.toBeFunction(), - onError: expect.toBeFunction(), - }, - [] - ); + const viewport = { duration: '1h' }; + + dataSourceStore.initiateRequest({ + request, + query, + viewport, + onSuccess: () => {}, + onError: () => {}, + }); + + expect(customSource.initiateRequest).toBeCalledWith({ + request, + query, + viewport, + onSuccess: expect.toBeFunction(), + onError: expect.toBeFunction(), + }); }); it('throws error when attempting to initiate a request to a non-existent data source', () => { @@ -43,14 +42,12 @@ it('throws error when attempting to initiate a request to a non-existent data so const request = { viewport: { start: new Date(), end: new Date() }, settings: { fetchFromStartToEnd: true } }; expect(() => - dataSourceStore.initiateRequest( - { - request, - query: { source: 'some-name' }, - onSuccess: () => {}, - onError: () => {}, - }, - [] - ) + dataSourceStore.initiateRequest({ + request, + query: { source: 'some-name' }, + onSuccess: () => {}, + onError: () => {}, + viewport: request.viewport, + }) ).toThrowError(/some-name/); }); diff --git a/packages/core/src/data-module/data-source-store/dataSourceStore.ts b/packages/core/src/data-module/data-source-store/dataSourceStore.ts index 226dfbc54..104f7e607 100644 --- a/packages/core/src/data-module/data-source-store/dataSourceStore.ts +++ b/packages/core/src/data-module/data-source-store/dataSourceStore.ts @@ -1,11 +1,5 @@ -import { - DataSource, - DataSourceName, - DataSourceRequest, - DataStreamQuery, - RequestInformation, - RequestInformationAndRange, -} from '../types.d'; +import { MinimalViewPortConfig } from '@synchro-charts/core'; +import { DataSource, DataSourceName, DataSourceRequest, DataStreamQuery, RequestInformation } from '../types.d'; import { TimeSeriesDataRequest } from '../data-cache/requestTypes'; /** @@ -30,30 +24,31 @@ export default class DataSourceStore { public getRequestsFromQueries = ({ queries, request, + viewport, }: { queries: Query[]; request: TimeSeriesDataRequest; - }): RequestInformation[] => queries.map((query) => this.getRequestsFromQuery({ query, request })).flat(); + viewport: MinimalViewPortConfig; + }): RequestInformation[] => queries.map((query) => this.getRequestsFromQuery({ query, request, viewport })).flat(); public getRequestsFromQuery = ({ query, request, + viewport, }: { query: Query; request: TimeSeriesDataRequest; + viewport: MinimalViewPortConfig; }): RequestInformation[] => { const dataSource = this.getDataSource(query.source); return dataSource - .getRequestsFromQuery({ query, request }) + .getRequestsFromQuery({ query, request, viewport }) .map((request) => ({ ...request, cacheSettings: query.cacheSettings })); }; - public initiateRequest = ( - request: DataSourceRequest, - requestInformations: RequestInformationAndRange[] - ) => { + public initiateRequest = (request: DataSourceRequest) => { const dataSource = this.getDataSource(request.query.source); - dataSource.initiateRequest(request, requestInformations); + dataSource.initiateRequest(request); }; public registerDataSource = (dataSource: DataSource) => { diff --git a/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts b/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts index bf0f1c993..6ff7c1362 100644 --- a/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts +++ b/packages/core/src/data-module/subscription-store/subscriptionStore.spec.ts @@ -31,6 +31,7 @@ const MOCK_SUBSCRIPTION: Subscription = { fetchMostRecentBeforeStart: true, }, }, + viewport: { start: new Date(2000, 0, 0), end: new Date() }, fulfill: () => {}, }; diff --git a/packages/core/src/data-module/subscription-store/subscriptionStore.ts b/packages/core/src/data-module/subscription-store/subscriptionStore.ts index 21ebe80ac..79ceea467 100644 --- a/packages/core/src/data-module/subscription-store/subscriptionStore.ts +++ b/packages/core/src/data-module/subscription-store/subscriptionStore.ts @@ -64,11 +64,11 @@ export default class SubscriptionStore { }); } - const { queries, request } = subscription; + const { queries, request, viewport } = subscription; // Subscribe to changes from the data cache const unsubscribe = this.dataCache.subscribe( - this.dataSourceStore.getRequestsFromQueries({ queries, request }), + this.dataSourceStore.getRequestsFromQueries({ queries, request, viewport }), subscription.emit ); diff --git a/packages/core/src/data-module/types.d.ts b/packages/core/src/data-module/types.d.ts index d6dcdda2a..7b04c054b 100644 --- a/packages/core/src/data-module/types.d.ts +++ b/packages/core/src/data-module/types.d.ts @@ -53,13 +53,15 @@ export interface DataStream { export type DataSource = { // An identifier for the name of the source, i.e. 'site-wise', 'roci', etc.. name: DataSourceName; // this is unique - initiateRequest: (request: DataSourceRequest, requestInformations: RequestInformationAndRange[]) => void; + initiateRequest: (request: DataSourceRequest) => void; getRequestsFromQuery: ({ query, requestInfo, + viewport, }: { query: Query; request: TimeSeriesDataRequest; + viewport: MinimalViewPortConfig; }) => RequestInformation[]; }; @@ -68,6 +70,7 @@ export type DataStreamCallback = (dataStreams: DataStream[]) => void; export type QuerySubscription = { queries: Query[]; request: TimeSeriesDataRequest; + viewport: MinimalViewPortConfig; emit: DataStreamCallback; // Initiate requests for the subscription fulfill: () => void; @@ -89,11 +92,12 @@ export type AnyDataStreamQuery = DataStreamQuery & any; export type ErrorCallback = ({ id, resolution, error }) => void; -export type SubscriptionUpdate = Partial, 'emit'>>; +export type SubscriptionUpdate = Partial, 'emit', 'viewport'>>; export type DataSourceRequest = { request: TimeSeriesDataRequest; query: Query; + viewport: MinimalViewPortConfig; onSuccess: DataStreamCallback; onError: ErrorCallback; }; diff --git a/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts b/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts index 3459d5167..92d70b330 100644 --- a/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts +++ b/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts @@ -53,18 +53,16 @@ describe('initiateRequest', () => { const dataSource = createDataSource(mockSDK as IoTSiteWiseClient); - dataSource.initiateRequest( - { - onError: noop, - onSuccess: noop, - query: { - source: SITEWISE_DATA_SOURCE, - assets: [], - }, - request: LAST_MINUTE_REQUEST, + dataSource.initiateRequest({ + onError: noop, + onSuccess: noop, + query: { + source: SITEWISE_DATA_SOURCE, + assets: [], }, - [] - ); + request: LAST_MINUTE_REQUEST, + viewport: LAST_MINUTE_REQUEST, + }); expect(getAssetPropertyAggregates).not.toBeCalled(); expect(getAssetPropertyValue).not.toBeCalled(); @@ -93,15 +91,13 @@ describe('initiateRequest', () => { const onError = jest.fn(); const onSuccess = jest.fn(); - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: LAST_MINUTE_REQUEST, - }, - [] - ); + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: LAST_MINUTE_REQUEST, + viewport: LAST_MINUTE_REQUEST, + }); await flushPromises(); @@ -137,15 +133,13 @@ describe('initiateRequest', () => { const onError = jest.fn(); const onSuccess = jest.fn(); - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: LAST_MINUTE_REQUEST, - }, - [] - ); + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: LAST_MINUTE_REQUEST, + viewport: LAST_MINUTE_REQUEST, + }); await flushPromises(); @@ -187,15 +181,13 @@ describe('initiateRequest', () => { assets: [{ assetId: ASSET_ID, properties: [{ propertyId: PROPERTY_1 }, { propertyId: PROPERTY_2 }] }], }; - dataSource.initiateRequest( - { - onError: noop, - onSuccess: noop, - query, - request: LAST_MINUTE_REQUEST, - }, - [] - ); + dataSource.initiateRequest({ + onError: noop, + onSuccess: noop, + query, + request: LAST_MINUTE_REQUEST, + viewport: LAST_MINUTE_REQUEST, + }); expect(getAssetPropertyValue).toBeCalledTimes(2); @@ -230,15 +222,13 @@ describe('initiateRequest', () => { ], }; - dataSource.initiateRequest( - { - onError: noop, - onSuccess: noop, - query, - request: LAST_MINUTE_REQUEST, - }, - [] - ); + dataSource.initiateRequest({ + onError: noop, + onSuccess: noop, + query, + request: LAST_MINUTE_REQUEST, + viewport: LAST_MINUTE_REQUEST, + }); expect(getAssetPropertyValue).toBeCalledTimes(2); @@ -283,24 +273,24 @@ it('requests raw data if specified per asset property', async () => { const onError = jest.fn(); const onSuccess = jest.fn(); - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: { - viewport: { - duration: MINUTE_IN_MS * 55, - }, - settings: { - fetchFromStartToEnd: true, - fetchAggregatedData: true, - resolution: '1m', - }, + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: { + viewport: { + duration: HOUR_IN_MS * 55, + }, + settings: { + fetchFromStartToEnd: true, + fetchAggregatedData: true, + resolution: '1m', }, }, - [] - ); + viewport: { + duration: MINUTE_IN_MS * 55, + }, + }); await flushPromises(); @@ -448,31 +438,27 @@ describe('aggregated data', () => { const onError = jest.fn(); const onSuccess = jest.fn(); - const FIFTY_MINUTES = MINUTE_IN_MS * 50; - const FIFTY_FIVE_MINUTES = MINUTE_IN_MS * 55; - const FIFTY_HOURS = HOUR_IN_MS * 55; - - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: { - viewport: { - duration: FIFTY_FIVE_MINUTES, - }, - settings: { - fetchMostRecentBeforeEnd: false, - fetchAggregatedData: true, - resolution: { - [FIFTY_HOURS]: '1d', - [FIFTY_MINUTES]: '1h', - }, + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: { + viewport: { + duration: HOUR_IN_MS * 65, + }, + settings: { + fetchMostRecentBeforeEnd: false, + fetchAggregatedData: true, + resolution: { + [HOUR_IN_MS * 60]: '1d', + [MINUTE_IN_MS * 15]: '1h', }, }, }, - [] - ); + viewport: { + duration: HOUR_IN_MS * 59, + }, + }); await flushPromises(); @@ -540,28 +526,26 @@ describe('aggregated data', () => { const onError = jest.fn(); const onSuccess = jest.fn(); - const FIFTY_FIVE_MINUTES = MINUTE_IN_MS * 55; - const resolution = '1m'; - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: { - viewport: { - duration: FIFTY_FIVE_MINUTES, - }, - settings: { - fetchAggregatedData: true, - fetchFromStartToEnd: true, - resolution: resolution, - }, + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: { + viewport: { + duration: HOUR_IN_MS * 2, + }, + settings: { + fetchAggregatedData: true, + fetchFromStartToEnd: true, + resolution: resolution, }, }, - [] - ); + viewport: { + duration: MINUTE_IN_MS * 55, + }, + }); await flushPromises(); @@ -635,22 +619,22 @@ describe('aggregated data', () => { const onSuccess = jest.fn(); - dataSource.initiateRequest( - { - onError: () => {}, - onSuccess, - query, - request: { - viewport: { - duration: MINUTE_IN_MS * 55, - }, - settings: { - fetchFromStartToEnd: true, - }, + dataSource.initiateRequest({ + onError: () => {}, + onSuccess, + query, + request: { + viewport: { + duration: HOUR_IN_MS * 2, + }, + settings: { + fetchFromStartToEnd: true, }, }, - [] - ); + viewport: { + duration: MINUTE_IN_MS * 55, + }, + }); await flushPromises(); @@ -726,27 +710,27 @@ describe('aggregated data', () => { const onSuccess = jest.fn(); expect(() => { - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: { - viewport: { - duration: HOUR_IN_MS, - }, - settings: { - fetchAggregatedData: true, - fetchMostRecentBeforeEnd: false, - fetchFromStartToEnd: true, - resolution: { - [MINUTE_IN_MS]: 'not_a_valid_resolution', - }, + dataSource.initiateRequest({ + onError, + onSuccess, + query, + request: { + viewport: { + duration: HOUR_IN_MS, + }, + settings: { + fetchAggregatedData: true, + fetchMostRecentBeforeEnd: false, + fetchFromStartToEnd: true, + resolution: { + [MINUTE_IN_MS]: 'not_a_valid_resolution', }, }, }, - [] - ); + viewport: { + duration: HOUR_IN_MS, + }, + }); }).toThrow(); expect(onError).not.toBeCalled(); @@ -782,6 +766,8 @@ describe('gets requests from query', () => { }, }; - expect(dataSource.getRequestsFromQuery({ query, request })).toEqual([expect.objectContaining({ refId: REF_ID })]); + expect(dataSource.getRequestsFromQuery({ query, request, viewport: request.viewport })).toEqual([ + expect.objectContaining({ refId: REF_ID }), + ]); }); }); diff --git a/packages/core/src/iotsitewise/time-series-data/data-source.ts b/packages/core/src/iotsitewise/time-series-data/data-source.ts index 0c29c600c..158b9f774 100644 --- a/packages/core/src/iotsitewise/time-series-data/data-source.ts +++ b/packages/core/src/iotsitewise/time-series-data/data-source.ts @@ -130,21 +130,21 @@ export const createDataSource = (siteWise: IoTSiteWiseClient): DataSource { + initiateRequest: ({ query, request, viewport, onSuccess, onError }) => { if (request.settings?.fetchMostRecentBeforeEnd) { return client.getLatestPropertyDataPoint({ query, onSuccess, onError }); } - const start = viewportStartDate(request.viewport); - const end = viewportEndDate(request.viewport); - const resolution = determineResolution({ resolution: request.settings?.resolution, fetchAggregatedData: request.settings?.fetchAggregatedData, - start, - end, + start: viewportStartDate(viewport), + end: viewportEndDate(viewport), }); + const start = viewportStartDate(request.viewport); + const end = viewportEndDate(request.viewport); + const { aggregatedDataQueries, rawDataQueries, defaultResolutionDataQueries } = separateDataQueries(query); const requests = []; @@ -199,15 +199,12 @@ export const createDataSource = (siteWise: IoTSiteWiseClient): DataSource request())); }, - getRequestsFromQuery: ({ query, request }) => { - const start = viewportStartDate(request.viewport); - const end = viewportEndDate(request.viewport); - + getRequestsFromQuery: ({ query, request, viewport }) => { const resolution = determineResolution({ resolution: request.settings?.resolution, fetchAggregatedData: request.settings?.fetchAggregatedData, - start, - end, + start: viewportStartDate(viewport), + end: viewportEndDate(viewport), }); return query.assets.flatMap(({ assetId, properties }) => diff --git a/packages/core/src/testing/mock-data-source/data-source.ts b/packages/core/src/testing/mock-data-source/data-source.ts index 18e4053ed..0d39bb8b9 100644 --- a/packages/core/src/testing/mock-data-source/data-source.ts +++ b/packages/core/src/testing/mock-data-source/data-source.ts @@ -1,33 +1,35 @@ -import { DataStreamId } from '@synchro-charts/core'; -import { DataSource, DataStream, RequestInformation } from '../../data-module/types'; -import { OnRequestData } from '../../data-module/data-cache/requestTypes'; -import { SiteWiseLegacyDataStreamQuery } from './types'; +import { DataSource, DataSourceRequest, DataStream } from '../../data-module/types'; +import { SiteWiseDataStreamQuery } from '../../iotsitewise/time-series-data/types'; +import { SITEWISE_DATA_SOURCE } from '../../iotsitewise/time-series-data'; +import { toDataStreamId } from '../../iotsitewise/time-series-data/util/dataStreamId'; -/** - * Legacy SiteWise data source - * - * A temporary bridge between IoT App Kit, and the existing SiteWise Monitor design. - */ -/** @deprecated */ -export const createSiteWiseLegacyDataSource = ( - onRequestData: OnRequestData -): DataSource => ({ - name: 'site-wise', - getRequestsFromQuery: ({ query: { dataStreamInfos } }): RequestInformation[] => - dataStreamInfos.map(({ id, resolution }) => ({ id, resolution })), - initiateRequest: ({ query, request, onSuccess }, requestInformations) => { - query.dataStreamInfos - .filter((dataStreamInfo) => requestInformations.some((r) => r.id === dataStreamInfo.id)) - .forEach((info) => { - onRequestData({ - request, - resolution: info.resolution, - onError: () => {}, - onSuccess: (id: DataStreamId, dataStream: DataStream) => { - onSuccess([dataStream]); - }, - dataStreamId: info.id, - }); - }); - }, +// A simple mock data source, which will always immediately return a successful response of your choosing. +export const createMockSiteWiseDataSource = ( + { + dataStreams = [], + onRequestData = () => {}, + }: { + dataStreams?: DataStream[]; + onRequestData?: (props: any) => void; + } = { dataStreams: [], onRequestData: () => {} } +): DataSource => ({ + name: SITEWISE_DATA_SOURCE, + initiateRequest: jest.fn(({ query, request, onSuccess = () => {} }: DataSourceRequest) => { + query.assets.forEach(({ assetId, properties }) => + properties.forEach(({ propertyId }) => { + onRequestData({ assetId, propertyId, request }); + onSuccess(dataStreams); + }) + ); + }), + getRequestsFromQuery: ({ query }) => + query.assets + .map(({ assetId, properties }) => + properties.map(({ propertyId, refId }) => ({ + id: toDataStreamId({ assetId, propertyId }), + refId, + resolution: 0, + })) + ) + .flat(), }); diff --git a/packages/core/src/testing/mock-data-source/types.d.ts b/packages/core/src/testing/mock-data-source/types.d.ts index 421178360..f1cbc81cf 100644 --- a/packages/core/src/testing/mock-data-source/types.d.ts +++ b/packages/core/src/testing/mock-data-source/types.d.ts @@ -4,10 +4,3 @@ import { DataStreamQuery } from '../../data-module/types'; /** * Learn more about AWS IoT SiteWise assets at https://docs.aws.amazon.com/iot-sitewise/latest/userguide/industrial-asset-models.html */ - -// Temporary query which matches the current query structure (DataStreamInfo) used within SiteWise Monitor. -/** @deprecated */ -export interface SiteWiseLegacyDataStreamQuery extends DataStreamQuery { - source: 'site-wise-legacy'; - dataStreamInfos: DataStreamInfo[]; -} From e0a55bf3c7df8c2903a5bc8fc187fae13d7d8491 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Wed, 9 Feb 2022 21:28:30 -0800 Subject: [PATCH 2/9] ref --- .../zooms in.snap.png | Bin 12293 -> 26050 bytes .../zooms out.snap.png | Bin 12332 -> 11835 bytes .../iot-connector.spec.component.ts | 4 +- .../components/src/testing/renderChart.tsx | 1 - .../testing/testing-ground/testing-ground.tsx | 72 +-- .../data-module/IotAppKitDataModule.spec.ts | 103 ++-- .../src/data-module/IotAppKitDataModule.ts | 72 +-- .../data-module/data-cache/requestTypes.ts | 2 - .../data-source-store/dataSourceStore.spec.ts | 53 ++- .../data-source-store/dataSourceStore.ts | 16 +- .../subscription-store/subscriptionStore.ts | 4 +- packages/core/src/data-module/types.d.ts | 2 +- .../time-series-data/client/client.spec.ts | 82 +++- .../time-series-data/client/client.ts | 11 +- .../client/getAggregatedPropertyDataPoints.ts | 48 +- .../client/getHistoricalPropertyDataPoints.ts | 36 +- .../client/getLatestPropertyDataPoint.ts | 42 +- .../time-series-data/data-source.spec.ts | 442 +++++++++++++----- .../time-series-data/data-source.ts | 25 +- 19 files changed, 655 insertions(+), 360 deletions(-) diff --git a/packages/components/cypress/snapshots/integration/iot-connector/iot-connector.spec.component.ts/zooms in.snap.png b/packages/components/cypress/snapshots/integration/iot-connector/iot-connector.spec.component.ts/zooms in.snap.png index 05e115cbe7c3b5414ba8d7fff649ff6c8df85ec0..b549c991cfeb32cc05b8158669f9237dbadd8b48 100644 GIT binary patch literal 26050 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 X{)s=7Q=|)w_fsGgRN!f+Zn*vr<z diff --git a/packages/components/cypress/snapshots/integration/iot-connector/iot-connector.spec.component.ts/zooms out.snap.png b/packages/components/cypress/snapshots/integration/iot-connector/iot-connector.spec.component.ts/zooms out.snap.png index 827e2b40edf538bbebc8bc7ce870310784d5be10..b1224fa633a9c2aaa116ceb9d01868acfe3d1929 100644 GIT binary patch literal 11835 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 { cy.wait(SECOND_IN_MS * 2); - cy.matchImageSnapshot('zooms in', snapshotOptions); + cy.get(testChartContainerClassNameSelector).dblclick(); cy.wait(SECOND_IN_MS * 2); + cy.matchImageSnapshot('zooms in', snapshotOptions); + cy.get(testChartContainerClassNameSelector).dblclick({ shiftKey: true }); cy.wait(SECOND_IN_MS * 2); diff --git a/packages/components/src/testing/renderChart.tsx b/packages/components/src/testing/renderChart.tsx index 596fb34c2..33b65672c 100644 --- a/packages/components/src/testing/renderChart.tsx +++ b/packages/components/src/testing/renderChart.tsx @@ -44,7 +44,6 @@ const defaultSettings = { resolution: { [3 * MINUTE_IN_MS]: '1m', }, - fetchAggregatedData: true, }; const start = new Date(2022, 0, 0, 0, 0); diff --git a/packages/components/src/testing/testing-ground/testing-ground.tsx b/packages/components/src/testing/testing-ground/testing-ground.tsx index b97306c97..5b4fb18a4 100755 --- a/packages/components/src/testing/testing-ground/testing-ground.tsx +++ b/packages/components/src/testing/testing-ground/testing-ground.tsx @@ -53,74 +53,6 @@ export class TestingGround { render() { return (
-
-
-
-
- -
- -
-
- -
-
resolution:{' '} @@ -78,6 +146,8 @@ export class TestingGround { settings={{ resolution: this.resolution, requestBuffer: 1 }} />
+ + ); From 111efa822a104c4fb4d6b0fc51249bb9f379bb76 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Thu, 10 Feb 2022 11:39:56 -0800 Subject: [PATCH 6/9] ref --- packages/core/src/data-module/IotAppKitDataModule.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/core/src/data-module/IotAppKitDataModule.ts b/packages/core/src/data-module/IotAppKitDataModule.ts index c81749234..298301ce4 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.ts @@ -80,9 +80,6 @@ export class IotAppKitDataModule implements DataModule { request: TimeSeriesDataRequest; queries: DataStreamQuery[]; }) => { - const start = viewportStartDate(viewport); - const end = viewportEndDate(viewport); - const requestedStreams = this.dataSourceStore.getRequestsFromQueries({ queries, request }); const isRequestedDataStream = ({ id, resolution }: RequestInformation) => @@ -94,8 +91,8 @@ export class IotAppKitDataModule implements DataModule { .map(({ resolution, id, cacheSettings }) => { const dateRanges = getDateRangesToRequest({ store: this.dataCache.getState(), - start, - end, + start: viewportStartDate(viewport), + end: viewportEndDate(viewport), resolution, dataStreamId: id, cacheSettings: { ...this.cacheSettings, ...cacheSettings }, From ab717c27cd03702c2d057ac062fdb09b07e203f7 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Thu, 10 Feb 2022 12:02:35 -0800 Subject: [PATCH 7/9] ref --- packages/core/src/data-module/IotAppKitDataModule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/data-module/IotAppKitDataModule.ts b/packages/core/src/data-module/IotAppKitDataModule.ts index 298301ce4..f0225d60c 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.ts @@ -175,9 +175,9 @@ export class IotAppKitDataModule implements DataModule { subscriptionId: string, subscriptionUpdate: SubscriptionUpdate ): void => { - const subscription = this.subscriptions.getSubscription(subscriptionId); + const { emit, ...subscription } = this.subscriptions.getSubscription(subscriptionId); - const updatedSubscription = Object.assign({}, subscription, subscriptionUpdate) as Omit; + const updatedSubscription = Object.assign({}, subscription, subscriptionUpdate); if ('queries' in updatedSubscription) { this.subscriptions.updateSubscription(subscriptionId, { From b5a5f0497988651f67109722fe5601b61e5d5def Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Thu, 10 Feb 2022 13:16:37 -0800 Subject: [PATCH 8/9] Update dataSourceStore.ts --- .../core/src/data-module/data-source-store/dataSourceStore.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/data-module/data-source-store/dataSourceStore.ts b/packages/core/src/data-module/data-source-store/dataSourceStore.ts index 818b70ff1..226dfbc54 100644 --- a/packages/core/src/data-module/data-source-store/dataSourceStore.ts +++ b/packages/core/src/data-module/data-source-store/dataSourceStore.ts @@ -1,4 +1,3 @@ -import { MinimalViewPortConfig } from '@synchro-charts/core'; import { DataSource, DataSourceName, From 8243658481ce6010e4a382bc49b67e6539cd6554 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Thu, 10 Feb 2022 13:21:58 -0800 Subject: [PATCH 9/9] Update data-source.ts --- packages/core/src/iotsitewise/time-series-data/data-source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/iotsitewise/time-series-data/data-source.ts b/packages/core/src/iotsitewise/time-series-data/data-source.ts index 1bb8de086..0a0374251 100644 --- a/packages/core/src/iotsitewise/time-series-data/data-source.ts +++ b/packages/core/src/iotsitewise/time-series-data/data-source.ts @@ -29,7 +29,7 @@ export const determineResolution = ({ start: Date; end: Date; }): string => { - if (resolution != null ?? resolution !== '0') { + if (resolution != null && resolution !== '0') { const viewportTimeSpan = end.getTime() - start.getTime(); const resolutionOverride = resolution || DEFAULT_RESOLUTION_MAPPING;