docs: function fetchDatapointsCycle(sources) {
    const payload$ = sources.ACTION.filter(
      action => action.type === FETCH_DATAPOINTS,
    ).pluck('payload');
  
    const dataChannelId$ = payload$.pluck('dataChannelId');
    const deviceId$ = payload$.pluck('deviceId');
  
    const deviceKey$ = sources.STATE.pluck('devices')
      .combineLatest(deviceId$, (devices, deviceId) =>
        R.path([deviceId, 'deviceKey'])(devices),
      )
      .filter(exist);
  
    const datapoints$ = sources.STATE.pluck('datapoints').filter(exist);
  
    const query$ = datapoints$
      .combineLatest(dataChannelId$, (datapoint, dataChannelId) =>
        R.path([dataChannelId, 'query'])(datapoint),
      )
      .filter(exist)
      .startWith({});
  
    const request$ = Observable.combineLatest(
      deviceKey$.distinctUntilChanged(),
      deviceId$.distinctUntilChanged(),
      dataChannelId$.distinctUntilChanged(),
      query$.distinctUntilKeyChanged('start').distinctUntilKeyChanged('end'),
      (deviceKey, deviceId, dataChannelId, query) => ({
        url: `/api/devices/${deviceId}/datachannels/${dataChannelId}/datapoints`,
        method: 'GET',
        headers: { deviceKey },
        category: FETCH_DATAPOINTS,
        query,
      }),
    );
  
    const response$ = sources.HTTP.select(FETCH_DATAPOINTS).switchMap(success);
  
    // Remind: api response with dataChannelId will be better.
    const responseDataChannedId$ = response$
      .pluck('request', 'url')
      .map(R.pipe(R.match(/[\w]+(?=\/datapoints$)/), R.head));
  
    const action$ = response$
      .pluck('body', 'data')
      .zip(responseDataChannedId$, (data, dataChannelId) => ({
        data,
        dataChannelId,
      }))
      .map(setDatapoints);
  
    return {
      ACTION: action$,
      HTTP: request$,
    };
  }