diff --git a/packages/core/src/iotAppKit.ts b/packages/core/src/iotAppKit.ts index 7e21e77da..24ead2692 100644 --- a/packages/core/src/iotAppKit.ts +++ b/packages/core/src/iotAppKit.ts @@ -5,7 +5,7 @@ import { createSiteWiseAssetDataSource } from './iotsitewise/time-series-data/as import { SiteWiseAssetModule } from './asset-modules'; import { IoTAppKitInitInputs, IoTAppKitSession } from './interface.d'; import { createDataSource } from './iotsitewise/time-series-data'; -import { subscribeToTimeSeriesData } from './iotsitewise/time-series-data/coordinator'; +import { subscribeToTimeSeriesData } from './iotsitewise/time-series-data/subscribeToTimeSeriesData'; import { subscribeToAssetTree } from './asset-modules/coordinator'; /** diff --git a/packages/core/src/iotsitewise/__mocks__/asset.ts b/packages/core/src/iotsitewise/__mocks__/asset.ts index 556396a75..03b2c4ed4 100644 --- a/packages/core/src/iotsitewise/__mocks__/asset.ts +++ b/packages/core/src/iotsitewise/__mocks__/asset.ts @@ -1,4 +1,9 @@ -import { AssetState, AssetSummary, DescribeAssetResponse } from "@aws-sdk/client-iotsitewise"; +import { + AssetState, + AssetSummary, + DescribeAssetModelResponse, + DescribeAssetResponse +} from "@aws-sdk/client-iotsitewise"; import { ASSET_MODEL_ID } from "./assetModel"; export const ASSET_SUMMARY: AssetSummary = { @@ -61,3 +66,51 @@ export const sampleAssetDescription: DescribeAssetResponse = { assetCompositeModels: [], assetProperties: [] }; + +export const createAssetResponse = ({ + assetId, + assetModelId, +}: { + assetId: string; + assetModelId: string; +}): DescribeAssetResponse => ({ + assetId: assetId, + assetName: `${assetId}-name`, + assetModelId, + assetCreationDate: undefined, + assetLastUpdateDate: undefined, + assetStatus: undefined, + assetHierarchies: [], + assetProperties: [], + assetArn: undefined, +}); + +export const createAssetModelResponse = ({ + propertyId, + assetModelId, + propertyName = 'property-name', +}: { + propertyId: string; + assetModelId: string; + propertyName: string; +}): DescribeAssetModelResponse => ({ + assetModelId, + assetModelName: `${assetModelId}-name`, + assetModelDescription: undefined, + assetModelProperties: [ + { + id: propertyId, + dataType: 'DOUBLE', + name: propertyName, + unit: 'm/s', + type: undefined, + }, + ], + assetModelStatus: undefined, + assetModelCompositeModels: [], + assetModelHierarchies: [], + assetModelCreationDate: undefined, + assetModelLastUpdateDate: undefined, + assetModelArn: undefined, +}); + diff --git a/packages/core/src/iotsitewise/time-series-data/provider.ts b/packages/core/src/iotsitewise/time-series-data/provider.ts index 4b9810a66..5af87737a 100644 --- a/packages/core/src/iotsitewise/time-series-data/provider.ts +++ b/packages/core/src/iotsitewise/time-series-data/provider.ts @@ -1,8 +1,7 @@ -import { MinimalViewPortConfig } from '@synchro-charts/core'; import { Provider, IoTAppKitComponentSession } from '../../interface'; import { AnyDataStreamQuery, DataModuleSubscription, SubscriptionUpdate } from '../../data-module/types'; import { datamodule } from '../..'; -import { subscribeToTimeSeriesData } from './coordinator'; +import { subscribeToTimeSeriesData } from './subscribeToTimeSeriesData'; import { TimeSeriesData } from './types'; /** diff --git a/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.spec.ts b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.spec.ts new file mode 100644 index 000000000..edb134c80 --- /dev/null +++ b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.spec.ts @@ -0,0 +1,231 @@ +import { subscribeToTimeSeriesData } from './subscribeToTimeSeriesData'; +import { IotAppKitDataModule } from '../../data-module/IotAppKitDataModule'; +import { SiteWiseAssetDataSource } from '../../data-module/types'; +import { createSiteWiseAssetDataSource } from './asset-data-source'; +import { createMockSiteWiseSDK } from '../__mocks__/iotsitewiseSDK'; +import { SiteWiseAssetModule } from '../../asset-modules'; +import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; +import flushPromises from 'flush-promises'; +import { createDataSource } from './data-source'; +import { createAssetModelResponse, createAssetResponse } from '../__mocks__/asset'; +import { toDataStreamId } from './util/dataStreamId'; +import { ASSET_PROPERTY_VALUE_HISTORY } from '../__mocks__/assetPropertyValue'; + +const initializeSubscribeToTimeSeriesData = (client: IoTSiteWiseClient) => { + const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(client); + const siteWiseAssetModule = new SiteWiseAssetModule(assetDataSource); + const siteWiseAssetModuleSession = siteWiseAssetModule.startSession(); + const dataModule = new IotAppKitDataModule(); + dataModule.registerDataSource(createDataSource(client)); + + return subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession); +}; + +it('does not emit any data streams when empty query is subscribed to', async () => { + const subscribe = initializeSubscribeToTimeSeriesData(createMockSiteWiseSDK()); + const cb = jest.fn(); + subscribe({ queries: [], request: { viewport: { duration: '5m' } } }, cb); + + await flushPromises(); + + expect(cb).not.toBeCalled(); +}); + +it('unsubscribes', () => { + const assetDataSource: SiteWiseAssetDataSource = createSiteWiseAssetDataSource(createMockSiteWiseSDK()); + const siteWiseAssetModule = new SiteWiseAssetModule(assetDataSource); + const siteWiseAssetModuleSession = siteWiseAssetModule.startSession(); + const dataModule = new IotAppKitDataModule(); + + const unsubscribeSpy = jest.fn(); + jest.spyOn(dataModule, 'subscribeToDataStreams').mockImplementation(() => ({ + unsubscribe: unsubscribeSpy, + update: () => {}, + })); + + const subscribe = subscribeToTimeSeriesData(dataModule, siteWiseAssetModuleSession); + const { unsubscribe } = subscribe({ queries: [], request: { viewport: { duration: '5m' } } }, () => {}); + unsubscribe(); + + expect(unsubscribeSpy).toBeCalled(); +}); + +it('provides time series data from iotsitewise', async () => { + const ASSET_ID = 'some-asset-id'; + const PROPERTY_ID = 'some-property-id'; + const ASSET_MODEL_ID = 'some-asset-model-id'; + const PROPERTY_NAME = 'some-property-name'; + + const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); + const describeAsset = jest + .fn() + .mockImplementation(({ assetId }) => + Promise.resolve(createAssetResponse({ assetId: assetId as string, assetModelId: ASSET_MODEL_ID })) + ); + const describeAssetModel = jest.fn().mockImplementation(({ assetModelId }) => + Promise.resolve( + createAssetModelResponse({ + assetModelId: assetModelId as string, + propertyId: PROPERTY_ID, + propertyName: PROPERTY_NAME, + }) + ) + ); + + const subscribe = initializeSubscribeToTimeSeriesData( + createMockSiteWiseSDK({ + describeAsset, + describeAssetModel, + getAssetPropertyValueHistory, + }) + ); + + const cb = jest.fn(); + subscribe( + { + queries: [ + { + source: 'site-wise', + assets: [ + { + assetId: ASSET_ID, + properties: [{ propertyId: PROPERTY_ID, resolution: '0' }], + }, + ], + }, + ], + request: { viewport: { duration: '5m' } }, + }, + cb + ); + + await flushPromises(); + + // fetches the asset summary + expect(describeAsset).toBeCalledTimes(1); + expect(describeAsset).toBeCalledWith({ assetId: ASSET_ID }); + + // fetches the asset model + expect(describeAssetModel).toBeCalledTimes(1); + expect(describeAssetModel).toBeCalledWith({ assetModelId: ASSET_MODEL_ID }); + + // fetches historical data + expect(getAssetPropertyValueHistory).toBeCalledTimes(1); + expect(getAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_ID, + }) + ); + + // provides the time series data + expect(cb).toHaveBeenLastCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: toDataStreamId({ assetId: ASSET_ID, propertyId: PROPERTY_ID }), + name: PROPERTY_NAME, + data: [ + { x: 1000099, y: 10.123 }, + { x: 2000000, y: 12.01 }, + ], + dataType: 'NUMBER', + unit: 'm/s', + }), + ], + }) + ); +}); + +it('provides timeseries data from iotsitewise when subscription is updated', async () => { + const ASSET_ID = 'some-asset-id'; + const PROPERTY_ID = 'some-property-id'; + const ASSET_MODEL_ID = 'some-asset-model-id'; + const PROPERTY_NAME = 'some-property-name'; + + const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); + const describeAsset = jest + .fn() + .mockImplementation(({ assetId }) => + Promise.resolve(createAssetResponse({ assetId: assetId as string, assetModelId: ASSET_MODEL_ID })) + ); + const describeAssetModel = jest.fn().mockImplementation(({ assetModelId }) => + Promise.resolve( + createAssetModelResponse({ + assetModelId: assetModelId as string, + propertyId: PROPERTY_ID, + propertyName: PROPERTY_NAME, + }) + ) + ); + + const subscribe = initializeSubscribeToTimeSeriesData( + createMockSiteWiseSDK({ + describeAsset, + describeAssetModel, + getAssetPropertyValueHistory, + }) + ); + + const cb = jest.fn(); + const { update } = subscribe( + { + queries: [], + request: { viewport: { duration: '5m' } }, + }, + cb + ); + + await flushPromises(); + + update({ + queries: [ + { + source: 'site-wise', + assets: [ + { + assetId: ASSET_ID, + properties: [{ propertyId: PROPERTY_ID, resolution: '0' }], + }, + ], + }, + ], + }); + + await flushPromises(); + + // fetches the asset summary + expect(describeAsset).toBeCalledTimes(1); + expect(describeAsset).toBeCalledWith({ assetId: ASSET_ID }); + + // fetches the asset model + expect(describeAssetModel).toBeCalledTimes(1); + expect(describeAssetModel).toBeCalledWith({ assetModelId: ASSET_MODEL_ID }); + + // fetches historical data + expect(getAssetPropertyValueHistory).toBeCalledTimes(1); + expect(getAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_ID, + }) + ); + + // provides the time series data + expect(cb).toHaveBeenLastCalledWith( + expect.objectContaining({ + dataStreams: [ + expect.objectContaining({ + id: toDataStreamId({ assetId: ASSET_ID, propertyId: PROPERTY_ID }), + name: PROPERTY_NAME, + data: [ + { x: 1000099, y: 10.123 }, + { x: 2000000, y: 12.01 }, + ], + dataType: 'NUMBER', + unit: 'm/s', + }), + ], + }) + ); +}); diff --git a/packages/core/src/iotsitewise/time-series-data/coordinator.ts b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts similarity index 61% rename from packages/core/src/iotsitewise/time-series-data/coordinator.ts rename to packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts index cbbad56df..8bbfdfd80 100644 --- a/packages/core/src/iotsitewise/time-series-data/coordinator.ts +++ b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts @@ -33,23 +33,28 @@ export const subscribeToTimeSeriesData = emit(); }); - queries.forEach((query) => { - query.assets.forEach((asset) => { - assetModuleSession - .fetchAssetSummary({ assetId: asset.assetId }) - .then((assetSummary) => { - if (assetSummary && assetSummary.assetModelId != null) { - return assetModuleSession.fetchAssetModel({ assetModelId: assetSummary.assetModelId }); - } - }) - .then((assetModelResponse) => { - if (assetModelResponse) { - assetModels[asset.assetId] = assetModelResponse; - emit(); - } + const fetchResources = ({ queries }: { queries?: SiteWiseDataStreamQuery[] }) => { + if (queries) { + queries.forEach((query) => { + query.assets.forEach((asset) => { + assetModuleSession + .fetchAssetSummary({ assetId: asset.assetId }) + .then((assetSummary) => { + if (assetSummary && assetSummary.assetModelId != null) { + return assetModuleSession.fetchAssetModel({ assetModelId: assetSummary.assetModelId }); + } + }) + .then((assetModelResponse) => { + if (assetModelResponse) { + assetModels[asset.assetId] = assetModelResponse; + emit(); + } + }); }); - }); - }); + }); + } + }; + fetchResources({ queries }); return { unsubscribe: () => { @@ -57,6 +62,7 @@ export const subscribeToTimeSeriesData = }, update: (subscriptionUpdate: SubscriptionUpdate) => { update(subscriptionUpdate); + fetchResources(subscriptionUpdate); }, }; }; diff --git a/packages/core/src/testing/index.ts b/packages/core/src/testing/index.ts index bd23f4f2f..8c5f7e7bd 100644 --- a/packages/core/src/testing/index.ts +++ b/packages/core/src/testing/index.ts @@ -1,2 +1 @@ export * from './wait'; -export * from '../asset-modules/mocks';