<commit_before>(function($) {
    $.entwine('ss', function($) {
        $('.WidgetAreaEditor.AdvancedWidgetAreaEditor').entwine({
            addAdvancedWidget: function(className, holder) {
                if($('#WidgetAreaEditor-'+holder).attr('maxwidgets')) {
                    var maxCount = $('#WidgetAreaEditor-'+holder).attr('maxwidgets');
                    var count = $('#usedWidgets-'+holder+' .Widget').length;
                    if (count+1 > maxCount) {
                        alert(ss.i18n._t('WidgetAreaEditor.TOOMANY'));
                        return;
                    }
                }

                //If Upload Fields are present ensure the enctype is right
                if($(this).find('.ss-uploadfield').length>0) {
                    $(this).closest('form').attr('enctype', 'multipart/form-data');
                }

                var parentRef=$(this);
                var locale=$(this).closest('form').find('input[name=Locale]').val();
                var url=$(this).attr('data-addlink');
                url=url.replace(/\?(.*)$/, '');
                var params=$(this).attr('data-addlink');
                if(params.indexOf('?')!=-1) {
                    params=params.replace(/^(.*)\?/, '?');
                }else {
                    params='';
                }

                $.ajax({
                    'url':url+'/'+className+params,
                    'success':function(response) {parentRef.insertWidgetEditor(response);},
                    'data':{
                        'locale':locale
                    },
                });
            },
            insertWidgetEditor: function(response) {
                this._super(response);

                //If Upload Fields are present ensure the enctype is right
                if($(this).find('.ss-uploadfield').length>0) {
                    $(this).closest('form').attr('enctype', 'multipart/form-data');
                }
            },
            rewriteWidgetAreaAttributes: function() {
                //Do nothing the widgets should be written correctly coming from the cms
            },
            insertWidgetEditor: function(response) {
                var usedWidgets = $('#usedWidgets-'+$(this).attr('name')).children();

                // Give the widget a unique id
                var newID=parseInt($(this).data('maxid'))+1;
                $(this).data('maxid', newID);

                var widgetContent=response.replace(/Widget\[(.*?)\]\[0\]/gi, "Widget[$1][new-" + (newID) + "]");
                widgetContent=widgetContent.replace(new RegExp('Widget-' + ($(this).attr('name')) + '-0-','gi'), "Widget-" + ($(this).attr('name')) + "-new-" + (newID) + "-");
                $('#usedWidgets-'+$(this).attr('name')).append(widgetContent);

                this.rewriteWidgetAreaAttributes();
            }
        });

        $('.AdvancedWidgetAreaEditor div.availableWidgets .Widget h3').entwine({
            onclick: function(event) {
                parts = $(this).parent().attr('id').split('-');
                var widgetArea = parts.pop();
                var className = parts.pop();
                $('#WidgetAreaEditor-'+widgetArea).addAdvancedWidget(className, widgetArea);

                event.stopPropagation();
                return false;
            }
        });
    });
})(jQuery);<commit_msg>Removed duplicate method<commit_after>(function($) {
    $.entwine('ss', function($) {
        $('.WidgetAreaEditor.AdvancedWidgetAreaEditor').entwine({
            onadd: function() {
                this._super();

                //If Upload Fields are present ensure the enctype is right
                if($(this).find('.ss-uploadfield').length>0) {
                    $(this).closest('form').attr('enctype', 'multipart/form-data');
                }
            },
            addAdvancedWidget: function(className, holder) {
                if($('#WidgetAreaEditor-'+holder).attr('maxwidgets')) {
                    var maxCount = $('#WidgetAreaEditor-'+holder).attr('maxwidgets');
                    var count = $('#usedWidgets-'+holder+' .Widget').length;
                    if (count+1 > maxCount) {
                        alert(ss.i18n._t('WidgetAreaEditor.TOOMANY'));
                        return;
                    }
                }

                //If Upload Fields are present ensure the enctype is right
                if($(this).find('.ss-uploadfield').length>0) {
                    $(this).closest('form').attr('enctype', 'multipart/form-data');
                }

                var parentRef=$(this);
                var locale=$(this).closest('form').find('input[name=Locale]').val();
                var url=$(this).attr('data-addlink');
                url=url.replace(/\?(.*)$/, '');
                var params=$(this).attr('data-addlink');
                if(params.indexOf('?')!=-1) {
                    params=params.replace(/^(.*)\?/, '?');
                }else {
                    params='';
                }

                $.ajax({
                    'url':url+'/'+className+params,
                    'success':function(response) {parentRef.insertWidgetEditor(response);},
                    'data':{
                        'locale':locale
                    },
                });
            },
            rewriteWidgetAreaAttributes: function() {
                //Do nothing the widgets should be written correctly coming from the cms
            },
            insertWidgetEditor: function(response) {
                var usedWidgets = $('#usedWidgets-'+$(this).attr('name')).children();

                // Give the widget a unique id
                var newID=parseInt($(this).data('maxid'))+1;
                $(this).data('maxid', newID);

                var widgetContent=response.replace(/Widget\[(.*?)\]\[0\]/gi, "Widget[$1][new-" + (newID) + "]");
                widgetContent=widgetContent.replace(new RegExp('Widget-' + ($(this).attr('name')) + '-0-','gi'), "Widget-" + ($(this).attr('name')) + "-new-" + (newID) + "-");
                $('#usedWidgets-'+$(this).attr('name')).append(widgetContent);

                //If Upload Fields are present ensure the enctype is right
                if($(this).find('.ss-uploadfield').length>0) {
                    $(this).closest('form').attr('enctype', 'multipart/form-data');
                }

                this.rewriteWidgetAreaAttributes();
            }
        });

        $('.AdvancedWidgetAreaEditor div.availableWidgets .Widget h3').entwine({
            onclick: function(event) {
                parts = $(this).parent().attr('id').split('-');
                var widgetArea = parts.pop();
                var className = parts.pop();
                $('#WidgetAreaEditor-'+widgetArea).addAdvancedWidget(className, widgetArea);

                event.stopPropagation();
                return false;
            }
        });
    });
})(jQuery);<|endoftext|>


<commit_before>import { getSourceJsFileListFromPathList } from '@dr-js/dev/module/node/filePreset.js'
import { initOutput, packOutput, clearOutput, verifyNoGitignore, verifyGitStatusClean, verifyOutputBin, verifyPackageVersionStrict, publishPackage } from '@dr-js/dev/module/output.js'
import { getTerserOption, minifyFileListWithTerser } from '@dr-js/dev/module/minify.js'
import { processFileList, fileProcessorBabel } from '@dr-js/dev/module/fileProcessor.js'

import { withRetry } from 'source/common/function.js'
import { runKit, argvFlag } from 'source/node/kit.js'

const retryCount = process.platform === 'linux'
  ? 1 // one chance should be enough for linux
  : 6 // 6 more chance for win32/darwin, since some net/fs test is still flaky
const retryTest = (func, ...args) => withRetry((failed, maxRetry) => {
  try { return func(...args) } catch (error) {
    console.error(`##[warning] [retry|${failed}/${maxRetry}]`, error) // https://github.com/actions/runner/blob/v2.278.0/src/Runner.Worker/ExecutionContext.cs#L1021-L1028
  }
}, retryCount, 1000)

runKit(async (kit) => {
  const processOutput = async ({ kit }) => {
    const fileListFull = await getSourceJsFileListFromPathList([ 'module', 'library', 'bin' ], kit.fromOutput)
    const fileListNoBrowser = fileListFull.filter((path) => !path.endsWith('Dr.browser.js'))

    let sizeReduce = 0
    sizeReduce += await minifyFileListWithTerser({ fileList: fileListFull, option: getTerserOption({ isReadable: true }), kit })
    sizeReduce += await processFileList({ fileList: fileListNoBrowser, processor: fileProcessorBabel, kit })
    kit.padLog(`size reduce: ${sizeReduce}B`)
  }

  await verifyNoGitignore({ path: kit.fromRoot('source'), kit })
  await verifyNoGitignore({ path: kit.fromRoot('source-bin'), kit })
  const packageJSON = await initOutput({ kit })
  if (!argvFlag('pack')) return

  const isPublish = argvFlag('publish')
  isPublish && verifyPackageVersionStrict(packageJSON.version)
  kit.padLog('generate spec & index.js')
  kit.RUN('npm run script-generate-spec')
  kit.padLog('build library-webpack')
  kit.RUN('npm run build-library-webpack')
  kit.padLog('build library-babel')
  kit.RUN('npm run build-library-babel')
  kit.padLog('build module')
  kit.RUN('npm run build-module')
  kit.padLog('build bin')
  kit.RUN('npm run build-bin')

  await processOutput({ kit })
  const isTest = argvFlag('test', 'publish')
  isTest && kit.padLog('lint source')
  isTest && kit.RUN('npm run lint')
  isTest && await processOutput({ kit }) // once more
  isTest && kit.padLog('test output')
  isTest && await retryTest(kit.RUN, 'npm run test-output-library')
  isTest && await retryTest(kit.RUN, 'npm run test-output-module')
  isTest && await retryTest(kit.RUN, 'npm run test-output-bin')
  isTest && kit.padLog('test browser')
  isTest && await retryTest(kit.RUN, 'npm run test-browser')
  isTest && kit.padLog('test bin')
  isTest && kit.RUN('npm run test-bin')
  await clearOutput({ kit })
  await verifyOutputBin({ packageJSON, kit })
  isTest && await verifyGitStatusClean({ kit })
  const pathPackagePack = await packOutput({ kit })
  isPublish && await publishPackage({ packageJSON, pathPackagePack, kit })
})
<commit_msg>CHG: code sort<commit_after>import { getSourceJsFileListFromPathList } from '@dr-js/dev/module/node/filePreset.js'
import { initOutput, packOutput, clearOutput, verifyNoGitignore, verifyGitStatusClean, verifyOutputBin, verifyPackageVersionStrict, publishPackage } from '@dr-js/dev/module/output.js'
import { getTerserOption, minifyFileListWithTerser } from '@dr-js/dev/module/minify.js'
import { processFileList, fileProcessorBabel } from '@dr-js/dev/module/fileProcessor.js'

import { withRetry } from 'source/common/function.js'
import { runKit, argvFlag } from 'source/node/kit.js'

const retryCount = process.platform === 'linux'
  ? 1 // one chance should be enough for linux
  : 6 // 6 more chance for win32/darwin, since some net/fs test is still flaky
const retryNpmRunTest = (kit, name) => withRetry((failed, maxRetry) => {
  try { return kit.RUN(`npm run ${name}`) } catch (error) {
    console.error(`##[warning] [retry|${name}|${failed}/${maxRetry}]`, error) // https://github.com/actions/runner/blob/v2.278.0/src/Runner.Worker/ExecutionContext.cs#L1021-L1028
  }
}, retryCount)

runKit(async (kit) => {
  const processOutput = async ({ kit }) => {
    const fileListFull = await getSourceJsFileListFromPathList([ 'module', 'library', 'bin' ], kit.fromOutput)
    const fileListNoBrowser = fileListFull.filter((path) => !path.endsWith('Dr.browser.js'))

    let sizeReduce = 0
    sizeReduce += await minifyFileListWithTerser({ fileList: fileListFull, option: getTerserOption({ isReadable: true }), kit })
    sizeReduce += await processFileList({ fileList: fileListNoBrowser, processor: fileProcessorBabel, kit })
    kit.padLog(`size reduce: ${sizeReduce}B`)
  }

  await verifyNoGitignore({ path: kit.fromRoot('source'), kit })
  await verifyNoGitignore({ path: kit.fromRoot('source-bin'), kit })
  const packageJSON = await initOutput({ kit })
  if (!argvFlag('pack')) return

  const isPublish = argvFlag('publish')
  isPublish && verifyPackageVersionStrict(packageJSON.version)
  kit.padLog('generate spec & index.js')
  kit.RUN('npm run script-generate-spec')
  kit.padLog('build library-webpack')
  kit.RUN('npm run build-library-webpack')
  kit.padLog('build library-babel')
  kit.RUN('npm run build-library-babel')
  kit.padLog('build module')
  kit.RUN('npm run build-module')
  kit.padLog('build bin')
  kit.RUN('npm run build-bin')

  await processOutput({ kit })
  const isTest = argvFlag('test', 'publish')
  isTest && kit.padLog('lint source')
  isTest && kit.RUN('npm run lint')
  isTest && await processOutput({ kit }) // once more
  isTest && kit.padLog('test output')
  isTest && await retryNpmRunTest(kit, 'test-output-library')
  isTest && await retryNpmRunTest(kit, 'test-output-module')
  isTest && await retryNpmRunTest(kit, 'test-output-bin')
  isTest && kit.padLog('test browser')
  isTest && await retryNpmRunTest(kit, 'test-browser')
  isTest && kit.padLog('test bin')
  isTest && kit.RUN('npm run test-bin')
  await clearOutput({ kit })
  await verifyOutputBin({ packageJSON, kit })
  isTest && await verifyGitStatusClean({ kit })
  const pathPackagePack = await packOutput({ kit })
  isPublish && await publishPackage({ packageJSON, pathPackagePack, kit })
})
<|endoftext|>


<commit_before>'use strict';
const EventEmitter = require('events');
const d3Array = require('d3-array');
const d3Scale = require('d3-scale');
const utils = require('./utils');
const config = require('./constants');

/*
http://192.168.1.240/debug/clip.html (IP of bridge)
Hue System API:
https://developers.meethue.com/documentation/core-concepts
*/

const upperThreshold = config.MAX_USABLE_DISTANCE;
const rangeLength = config.MAX_USABLE_DISTANCE - config.MIN_USABLE_DISTANCE;
const sampleSize = 10;
const numberOfStoredValues = 200;
var maxMovement = 0;
var exitThresholdScale = d3Scale.scaleLinear();
exitThresholdScale.domain([6, config.MAX_USABLE_DISTANCE]);
exitThresholdScale.range([4, 15]);

const quantizeScale = d3Scale.scaleQuantize()
  .domain([config.MIN_USABLE_DISTANCE, config.MAX_USABLE_DISTANCE])
  .range(
    d3Array.range(2, 24, 1)
  );

const SerialPort = require('serialport');
const rawValueIsAMiss = function(rawVal){
  return rawVal > upperThreshold;
};

class DistanceSensor extends EventEmitter {
  constructor({serialPath, lightId}){
    super();
    this.lightId = lightId;
    this.init(serialPath);
    return this;
  }

  getState(){
    var state = {};
    ['distance', 'velocity', 'occupantsCount', 'isMoving', 'isEmpty', 'movementLong', 'movementShort']
    .forEach(prop => {
      state[prop] = this[prop];
    });
    return state;
  }

  init(serialPath){
    this.serialPath = serialPath;
    this.setInitialState();
    this.vals = [];
    this.vals.length = numberOfStoredValues;
    this.vals.fill(config.MAX_USABLE_DISTANCE);
    this.timestamps = [];
    this.timestamps.length = numberOfStoredValues;
    this.timestamps.fill(Date.now());
    this.rawVals = [];
    this.rawVals.length = numberOfStoredValues;
    this.rawVals.fill(upperThreshold);
    this.velocityVals = [];
    this.velocityVals.length = 10;
    this.velocityVals.fill(0);
    process.nextTick(
      () => {
        this.update();
        this.send();
      }
    )

    this.port = new SerialPort(this.serialPath,{
      // this sensor prepends 'R' before each value
      parser: SerialPort.parsers.readline('R')
    });

    this.port
      .on('open', () => {
        console.log(`${this.serialPath} channel opened`);
      })
      .on('error', err => {
        console.error(`Could not open serial port: ${err.message}`);
      });

    setTimeout(
      () => {
        this.port
          .on('data', data => this.on_data(data));
      },
      1000
    );
  }

  setInitialState(){
    this.hasTarget = false;
    this.velocity = 0;
    this.distance = upperThreshold;
    this.occupantsCount = 0;
    this.isMoving = false;
    this.isEmpty = true;
    this.movementShort = 0;
    this.movementLong = 0;
    this.exitTime = Date.now();
    this.sampleTime = Date.now();
  }

  on_data(data){
    const distance = parseInt(data, 10);
    var addGoodValue = (v, silent) => {
      this.vals = [v].concat(this.vals).slice(0, numberOfStoredValues);
      this.timestamps = [Date.now()].concat(this.timestamps).slice(0, numberOfStoredValues);
      if(!silent){
        this.update();
        process.nextTick(this.send.bind(this));
      }
    }
    if(!isNaN(distance)){
      // save up to 100 values, which corresponds to 10 secs of data
      this.rawVals = [distance].concat(this.rawVals).slice(0,numberOfStoredValues);
      const exitThreshold = Math.round(exitThresholdScale(d3Array.mean(this.vals.slice(0,4))));
      if(this.hasTarget){
        // if the last X num of readings (based on distance) are misses, then we're empty
        if(!this.rawVals.slice(0, exitThreshold).find(v => !rawValueIsAMiss(v))){
          this.hasTarget = false;
          addGoodValue(config.MAX_USABLE_DISTANCE);
        }
        // we got a value beneath our threshold that is not too far out of bounds with the last 5 raw readings
        else if(!rawValueIsAMiss(distance) && Math.abs(distance - d3Array.mean(this.rawVals.filter(v => v < upperThreshold).slice(0,5))) < 10) {
          // we got a legit good value
          addGoodValue(distance);
        }
        else if(rawValueIsAMiss(distance)) {
          // we got a miss, but just fill it in with most recent good value
          addGoodValue(this.vals[0]);
        }
      } else {
        const avgDistance = d3Array.mean(this.rawVals.slice(0, 2));
        let recentRawVals = this.rawVals.slice(
          0,
          quantizeScale(avgDistance)
        );
        // if the last five are all legit, then something is there
        // TODO, make the # of legit values higher for further distances
        if( !recentRawVals.find(v => rawValueIsAMiss(v)) ) {
          // there's something there
          recentRawVals.reverse().forEach((v,i) => {
            addGoodValue(distance, i < recentRawVals.length-1);
          });
          this.hasTarget = true;
        }
      }
    }
  }

  send(){
    this.emit('state', this.getState());
  }

  triggerEvent(event, data){
    this.emit(event, data);
  }

  on_enter(){
    this.occupantsCount += 1;
    this.isEmpty = false;
    this.triggerEvent('enter');
  }

  on_exit(){
    this.occupantsCount = Math.max(this.occupantsCount - 1, 0);
    this.isEmpty = true;
    this.exitTime = Date.now();
    this.triggerEvent('exit');
  }

  on_movement(movementFactor){
    this.isMoving = true;
    this.triggerEvent('movement', movementFactor);
  }

  on_stillness(){
    this.isMoving = false;
    this.triggerEvent('stillness');
  }

  getMovementFactor(vals){
    const mean = d3Array.mean(vals);
    const smoothing = (mean - config.MIN_USABLE_DISTANCE) * 0.8 / rangeLength;
    var timestamps = [];
    vals = vals.filter(
      (v, i) => {
        if(v < config.MAX_USABLE_DISTANCE){
          timestamps.push(this.timestamps[i]);
          return true;
        }
        return false;
      }
    );
    return utils.wiggle(vals, timestamps, smoothing);
    // return utils.IQR(vals);
    // return utils.MAD(vals);
    // return Math.log(d3Array.variance(vals) + 1);
  }

  update(){
    const now = Date.now();
    this.distance = this.vals[0];
    // console.log(this.distance);
    // below than 10 is noise, 10 - 50 is the real movement range
    this.velocity = this.vals.slice(0, sampleSize*2).find(v => v === config.MAX_USABLE_DISTANCE) ?
      0 :
      Math.round(
        (d3Array.mean(this.vals.slice(0, sampleSize)) - d3Array.mean(this.vals.slice(sampleSize,sampleSize*2)))
        /(now - this.sampleTime)
        /sampleSize*1000
      );
    this.velocity = isNaN(this.velocity) ? 0 : this.velocity;
    // this.velocityVals = [isNaN(velocity) ? 0 : velocity].concat(this.velocityVals).slice(0,10);
    // calculate movementFactor from variance
    // this.movementShort = this.getMovementFactor(this.vals.slice(0,Math.round(sampleSize/2)));
    this.movementLong = this.getMovementFactor(this.vals.slice(0,sampleSize*2));
    /*
    if empty
      listen for enter event
      listen for movement, which should also trigger an enter
    else if not empty
      listen for exit event
      listen for stillness
    */

    if(this.isEmpty){
      if(this.distance < config.MAX_USABLE_DISTANCE){
        this.on_enter();
      }
      // long term movement factor
    } else {
      if(this.distance === config.MAX_USABLE_DISTANCE){
        this.on_exit();
      } else if(this.isMoving && this.movementShort < 1){
        this.on_stillness();
      } else if(this.movementShort >= 1){
        this.on_movement(this.movementShort);
      }
    }

    this.sampleTime = now;

  }

}

module.exports = DistanceSensor;
<commit_msg>emitting exit on start, and re-instating movementShort<commit_after>'use strict';
const EventEmitter = require('events');
const d3Array = require('d3-array');
const d3Scale = require('d3-scale');
const utils = require('./utils');
const config = require('./constants');

/*
http://192.168.1.240/debug/clip.html (IP of bridge)
Hue System API:
https://developers.meethue.com/documentation/core-concepts
*/

const upperThreshold = config.MAX_USABLE_DISTANCE;
const rangeLength = config.MAX_USABLE_DISTANCE - config.MIN_USABLE_DISTANCE;
const sampleSize = 10;
const numberOfStoredValues = 200;
var maxMovement = 0;
var exitThresholdScale = d3Scale.scaleLinear();
exitThresholdScale.domain([6, config.MAX_USABLE_DISTANCE]);
exitThresholdScale.range([4, 15]);

const quantizeScale = d3Scale.scaleQuantize()
  .domain([config.MIN_USABLE_DISTANCE, config.MAX_USABLE_DISTANCE])
  .range(
    d3Array.range(2, 24, 1)
  );

const SerialPort = require('serialport');
const rawValueIsAMiss = function(rawVal){
  return rawVal > upperThreshold;
};

class DistanceSensor extends EventEmitter {
  constructor({serialPath, lightId}){
    super();
    this.lightId = lightId;
    this.init(serialPath);
    return this;
  }

  getState(){
    var state = {};
    ['distance', 'velocity', 'occupantsCount', 'isMoving', 'isEmpty', 'movementLong', 'movementShort']
    .forEach(prop => {
      state[prop] = this[prop];
    });
    return state;
  }

  init(serialPath){
    this.serialPath = serialPath;
    this.setInitialState();
    this.vals = [];
    this.vals.length = numberOfStoredValues;
    this.vals.fill(config.MAX_USABLE_DISTANCE);
    this.timestamps = [];
    this.timestamps.length = numberOfStoredValues;
    this.timestamps.fill(Date.now());
    this.rawVals = [];
    this.rawVals.length = numberOfStoredValues;
    this.rawVals.fill(upperThreshold);
    this.velocityVals = [];
    this.velocityVals.length = 10;
    this.velocityVals.fill(0);
    this.on_exit();
    process.nextTick(
      () => {
        this.update();
        this.send();
      }
    )

    this.port = new SerialPort(this.serialPath,{
      // this sensor prepends 'R' before each value
      parser: SerialPort.parsers.readline('R')
    });

    this.port
      .on('open', () => {
        console.log(`${this.serialPath} channel opened`);
      })
      .on('error', err => {
        console.error(`Could not open serial port: ${err.message}`);
      });

    setTimeout(
      () => {
        this.port
          .on('data', data => this.on_data(data));
      },
      1000
    );
  }

  setInitialState(){
    this.hasTarget = false;
    this.velocity = 0;
    this.distance = upperThreshold;
    this.occupantsCount = 0;
    this.isMoving = false;
    this.isEmpty = true;
    this.movementShort = 0;
    this.movementLong = 0;
    this.exitTime = Date.now();
    this.sampleTime = Date.now();
  }

  on_data(data){
    const distance = parseInt(data, 10);
    var addGoodValue = (v, silent) => {
      this.vals = [v].concat(this.vals).slice(0, numberOfStoredValues);
      this.timestamps = [Date.now()].concat(this.timestamps).slice(0, numberOfStoredValues);
      if(!silent){
        this.update();
        process.nextTick(this.send.bind(this));
      }
    }
    if(!isNaN(distance)){
      // save up to 100 values, which corresponds to 10 secs of data
      this.rawVals = [distance].concat(this.rawVals).slice(0,numberOfStoredValues);
      const exitThreshold = Math.round(exitThresholdScale(d3Array.mean(this.vals.slice(0,4))));
      if(this.hasTarget){
        // if the last X num of readings (based on distance) are misses, then we're empty
        if(!this.rawVals.slice(0, exitThreshold).find(v => !rawValueIsAMiss(v))){
          this.hasTarget = false;
          addGoodValue(config.MAX_USABLE_DISTANCE);
        }
        // we got a value beneath our threshold that is not too far out of bounds with the last 5 raw readings
        else if(!rawValueIsAMiss(distance) && Math.abs(distance - d3Array.mean(this.rawVals.filter(v => v < upperThreshold).slice(0,5))) < 10) {
          // we got a legit good value
          addGoodValue(distance);
        }
        else if(rawValueIsAMiss(distance)) {
          // we got a miss, but just fill it in with most recent good value
          addGoodValue(this.vals[0]);
        }
      } else {
        const avgDistance = d3Array.mean(this.rawVals.slice(0, 2));
        let recentRawVals = this.rawVals.slice(
          0,
          quantizeScale(avgDistance)
        );
        // if the last five are all legit, then something is there
        // TODO, make the # of legit values higher for further distances
        if( !recentRawVals.find(v => rawValueIsAMiss(v)) ) {
          // there's something there
          recentRawVals.reverse().forEach((v,i) => {
            addGoodValue(distance, i < recentRawVals.length-1);
          });
          this.hasTarget = true;
        }
      }
    }
  }

  send(){
    this.emit('state', this.getState());
  }

  triggerEvent(event, data){
    this.emit(event, data);
  }

  on_enter(){
    this.occupantsCount += 1;
    this.isEmpty = false;
    this.triggerEvent('enter');
  }

  on_exit(){
    this.occupantsCount = Math.max(this.occupantsCount - 1, 0);
    this.isEmpty = true;
    this.exitTime = Date.now();
    this.triggerEvent('exit');
  }

  on_movement(movementFactor){
    this.isMoving = true;
    this.triggerEvent('movement', movementFactor);
  }

  on_stillness(){
    this.isMoving = false;
    this.triggerEvent('stillness');
  }

  getMovementFactor(vals){
    const mean = d3Array.mean(vals);
    const smoothing = (mean - config.MIN_USABLE_DISTANCE) * 0.8 / rangeLength;
    var timestamps = [];
    vals = vals.filter(
      (v, i) => {
        if(v < config.MAX_USABLE_DISTANCE){
          timestamps.push(this.timestamps[i]);
          return true;
        }
        return false;
      }
    );
    return utils.wiggle(vals, timestamps, smoothing);
    // return utils.IQR(vals);
    // return utils.MAD(vals);
    // return Math.log(d3Array.variance(vals) + 1);
  }

  update(){
    const now = Date.now();
    this.distance = this.vals[0];
    // console.log(this.distance);
    // below than 10 is noise, 10 - 50 is the real movement range
    this.velocity = this.vals.slice(0, sampleSize*2).find(v => v === config.MAX_USABLE_DISTANCE) ?
      0 :
      Math.round(
        (d3Array.mean(this.vals.slice(0, sampleSize)) - d3Array.mean(this.vals.slice(sampleSize,sampleSize*2)))
        /(now - this.sampleTime)
        /sampleSize*1000
      );
    this.velocity = isNaN(this.velocity) ? 0 : this.velocity;
    // this.velocityVals = [isNaN(velocity) ? 0 : velocity].concat(this.velocityVals).slice(0,10);
    // calculate movementFactor from variance
    this.movementShort = this.getMovementFactor(this.vals.slice(0,Math.round(sampleSize/2)));
    this.movementLong = this.getMovementFactor(this.vals.slice(0,sampleSize*2));
    /*
    if empty
      listen for enter event
      listen for movement, which should also trigger an enter
    else if not empty
      listen for exit event
      listen for stillness
    */

    if(this.isEmpty){
      if(this.distance < config.MAX_USABLE_DISTANCE){
        this.on_enter();
      }
      // long term movement factor
    } else {
      if(this.distance === config.MAX_USABLE_DISTANCE){
        this.on_exit();
      } else if(this.isMoving && this.movementShort < 10){
        this.on_stillness();
      } else if(this.movementShort >= 10){
        this.on_movement(this.movementShort);
      }
    }

    this.sampleTime = now;

  }

}

module.exports = DistanceSensor;
<|endoftext|>


<commit_before>/* eslint-disable i18next/no-literal-string */
import {dia} from "jointjs"
import "jointjs/dist/joint.css"
import _, {cloneDeep, debounce, isEqual, sortBy} from "lodash"
import PropTypes from "prop-types"
import React from "react"
import {findDOMNode} from "react-dom"
import {getProcessCategory, getSelectionState} from "../../reducers/selectors/graph"
import {getLoggedUser, getProcessDefinitionData} from "../../reducers/selectors/settings"
import "../../stylesheets/graph.styl"
import {filterDragHovered, setLinksHovered} from "./dragHelpers"
import {updateNodeCounts} from "./EspNode/element"
import {GraphPaperContainer} from "./focusable"
import {createPaper, directedLayout, drawGraph, isBackgroundObject, isModelElement} from "./GraphPartialsInTS"
import styles from "./graphTheme.styl"
import * as GraphUtils from "./GraphUtils"
import {Events} from "./joint-events"
import NodeUtils from "./NodeUtils"
import {PanZoomPlugin} from "./PanZoomPlugin"
import {RangeSelectPlugin, SelectionMode} from "./RangeSelectPlugin"
import "./svg-export/export.styl"
import {prepareSvg} from "./svg-export/prepareSvg"

export class Graph extends React.Component {

  static propTypes = {
    processToDisplay: PropTypes.object.isRequired,
    loggedUser: PropTypes.object.isRequired,
    connectDropTarget: PropTypes.func,
    showModalNodeDetails: PropTypes.func.isRequired,
    showModalEdgeDetails: PropTypes.func.isRequired,
    isSubprocess: PropTypes.bool,
  }
  redrawing = false
  directedLayout = directedLayout.bind(this)
  createPaper = createPaper.bind(this)
  drawGraph = drawGraph.bind(this)
  forceLayout = debounce(() => {
    this.directedLayout(this.props.selectionState)
    this.panAndZoom.fitSmallAndLargeGraphs()
  }, 50)

  constructor(props) {
    super(props)

    this.graph = new dia.Graph()
    this.graph.on(Events.REMOVE, (e, f) => {
      if (e.isLink() && !this.redrawing) {
        this.props.actions.nodesDisconnected(e.attributes.source.id, e.attributes.target.id)
      }
    })
    this.nodesMoving()

    this.espGraphRef = React.createRef()
  }

  getEspGraphRef = () => {
    return this.espGraphRef.current
  }

  componentWillUnmount() {
    // force destroy event on model for plugins cleanup
    this.processGraphPaper.model.destroy()
    this.unbindEventHandlers()
  }

  bindEventHandlers() {
    this.changeNodeDetailsOnClick()

    this.processGraphPaper.on(Events.BLANK_POINTERUP, event => {
      if (!event.isPropagationStopped()) {
        if (this.props.fetchedProcessDetails != null) {
          this.props.actions.displayNodeDetails(this.props.fetchedProcessDetails.json.properties)
          this.props.actions.resetSelection()
        }
      }
    })
    this.hooverHandling()
  }

  unbindEventHandlers() {

  }

  componentDidMount() {
    this.processGraphPaper = this.createPaper()
    this.processGraphPaper.freeze()
    this.drawGraph(
      this.props.processToDisplay,
      this.props.layout,
      this.props.processDefinitionData,
    )
    this.processGraphPaper.unfreeze()
    this._prepareContentForExport()

    // event handlers binding below. order sometimes matters
    this.panAndZoom = new PanZoomPlugin(this.processGraphPaper)
    new RangeSelectPlugin(this.processGraphPaper)
    this.processGraphPaper.on("rangeSelect:selected", ({elements, mode}) => {
      const nodes = elements
        .filter(el => isModelElement(el))
        .map(({id}) => id)
      if (mode === SelectionMode.toggle) {
        this.props.actions.toggleSelection(...nodes)
      } else {
        this.props.actions.resetSelection(...nodes)
      }
    })

    this.bindEventHandlers()
    this.highlightNodes(this.props.processToDisplay, this.props.nodeToDisplay)
    this.updateNodesCounts()

    this.graph.on(Events.CHANGE_DRAG_OVER, () => {
      const links = this.graph.getLinks()
      links.forEach(l => this.unhighlightCell(l, styles.dragHovered))

      const [active] = filterDragHovered(links)

      if (active) {
        this.highlightCell(active, styles.dragHovered)
        active.toBack()
      }
    })

    //we want to inject node during 'Drag and Drop' from graph paper
    this.graph.on(Events.ADD, (cell) => {
      if (this.isNotLink(cell)) {
        this.handleInjectBetweenNodes(cell)
        setLinksHovered(this.graph)
      }
    })

    this.panAndZoom.fitSmallAndLargeGraphs()
  }

  isNotLink(cell) {
    return cell.attributes.type !== "link"
  }

  canAddNode(node) {
    return this.props.capabilities.write &&
      NodeUtils.isNode(node) &&
      NodeUtils.isAvailable(node, this.props.processDefinitionData, this.props.processCategory)
  }

  addNode(node, position) {
    if (this.canAddNode(node)) {
      this.props.actions.nodeAdded(node, position)
    }
  }

  componentWillUpdate(nextProps, nextState) {
    const processChanged = !isEqual(this.props.processToDisplay, nextProps.processToDisplay) ||
      !isEqual(this.props.layout, nextProps.layout) ||
      !isEqual(this.props.processDefinitionData, nextProps.processDefinitionData)
    if (processChanged) {
      this.drawGraph(
        nextProps.processToDisplay,
        nextProps.layout,
        nextProps.processDefinitionData,
      )
    }

    //when e.g. layout changed we have to remember to highlight nodes
    const nodeToDisplayChanged = !isEqual(this.props.nodeToDisplay, nextProps.nodeToDisplay)
    const selectedNodesChanged = !isEqual(this.props.selectionState, nextProps.selectionState)
    if (processChanged || nodeToDisplayChanged || selectedNodesChanged) {
      this.highlightNodes(nextProps.processToDisplay, nextProps.nodeToDisplay, nextProps.selectionState)
    }
  }

  componentDidUpdate(prevProps, prevState) {
    const {processCounts} = this.props
    if (!isEqual(processCounts, prevProps.processCounts)) {
      this.updateNodesCounts()
    }
  }

  updateNodesCounts() {
    const {processCounts} = this.props
    const nodes = this.graph.getElements().filter(isModelElement)
    nodes.forEach(updateNodeCounts(processCounts))
  }

  zoomIn() {
    this.panAndZoom.zoomIn()
  }

  zoomOut() {
    this.panAndZoom.zoomOut()
  }

  async exportGraph() {
    return await prepareSvg(this._exportGraphOptions)
  }

  validateConnection = (cellViewS, magnetS, cellViewT, magnetT, end, linkView) => {
    const from = cellViewS.model.id
    const to = cellViewT.model.id
    const previousEdge = linkView.model.attributes.edgeData || {}
    return magnetT && NodeUtils.canMakeLink(from, to, this.props.processToDisplay, this.props.processDefinitionData, previousEdge)
  }

  disconnectPreviousEdge = (from, to) => {
    if (this.graphContainsEdge(from, to)) {
      this.props.actions.nodesDisconnected(from, to)
    }
  }

  graphContainsEdge(from, to) {
    return this.props.processToDisplay.edges.some(edge => edge.from === from && edge.to === to)
  }

  handleInjectBetweenNodes = (middleMan) => {
    const links = this.graph.getLinks()
    const [linkBelowCell] = filterDragHovered(links)

    if (linkBelowCell && middleMan) {
      const source = this.graph.getCell(linkBelowCell.getSourceElement().id)
      const target = this.graph.getCell(linkBelowCell.getTargetElement().id)

      const middleManNode = middleMan.attributes.nodeData

      const sourceNode = source.attributes.nodeData
      const targetNode = target.attributes.nodeData

      if (GraphUtils.canInjectNode(
        this.props.processToDisplay,
        sourceNode.id,
        middleMan.id,
        targetNode.id,
        this.props.processDefinitionData,
      )) {
        //TODO: consider doing inject check in actions.js?
        this.props.actions.injectNode(
          sourceNode,
          middleManNode,
          targetNode,
          linkBelowCell.attributes.edgeData.edgeType,
        )
      }
    }
  }

  _prepareContentForExport = () => {
    const {options, defs} = this.processGraphPaper
    this._exportGraphOptions = {
      options: cloneDeep(options),
      defs: defs.cloneNode(true),
    }
  }

  highlightNodes = (data, nodeToDisplay, selectionState) => {
    this.graph.getCells().forEach(cell => {
      this.unhighlightCell(cell, "node-validation-error")
      this.unhighlightCell(cell, "node-focused")
      this.unhighlightCell(cell, "node-focused-with-validation-error")
    })

    const invalidNodeIds = _.keys((data.validationResult && data.validationResult.errors || {}).invalidNodes)
    const selectedNodeIds = selectionState || []

    invalidNodeIds.forEach(id => selectedNodeIds.includes(id) ?
      this.highlightNode(id, "node-focused-with-validation-error") :
      this.highlightNode(id, "node-validation-error"))

    selectedNodeIds.forEach(id => {
      if (!invalidNodeIds.includes(id)) {
        this.highlightNode(id, "node-focused")
      }
    })
  }

  highlightCell(cell, className) {
    this.processGraphPaper.findViewByModel(cell).highlight(null, {
      highlighter: {
        name: "addClass",
        options: {className: className},
      },
    })
  }

  unhighlightCell(cell, className) {
    this.processGraphPaper.findViewByModel(cell).unhighlight(null, {
      highlighter: {
        name: "addClass",
        options: {className: className},
      },
    })
  }

  highlightNode = (nodeId, highlightClass) => {
    const cell = this.graph.getCell(nodeId)
    if (cell) { //prevent `properties` node highlighting
      this.highlightCell(cell, highlightClass)
    }
  }

  changeLayoutIfNeeded = () => {
    const {layout, actions, isSubprocess} = this.props

    if (isSubprocess) {
      return
    }

    const elements = this.graph.getElements().filter(isModelElement)
    const newLayout = sortBy(
      elements.map(el => {
        const {x, y} = el.get("position")
        return {id: el.id, position: {x, y}}
      }),
      e => e.id,
    )
    if (!isEqual(layout, newLayout)) {
      actions?.layoutChanged(newLayout)
    }
  }

  changeNodeDetailsOnClick() {
    this.processGraphPaper.on(Events.CELL_POINTERDBLCLICK, (cellView) => {
      const nodeDataId = cellView.model.attributes.nodeData?.id
      if (nodeDataId) {
        const nodeData = this.getNodeData(cellView.model)
        const prefixedNodeId = this.props.nodeIdPrefixForSubprocessTests + nodeDataId
        this.props.showModalNodeDetails({...nodeData, id: prefixedNodeId}, this.props.readonly)
      }

      if (cellView.model.attributes.edgeData) {
        this.props.showModalEdgeDetails(cellView.model.attributes.edgeData)
      }
    })

    if (this.props.singleClickNodeDetailsEnabled) {
      this.processGraphPaper.on(Events.CELL_POINTERCLICK, (cellView, evt) => {

        const nodeDataId = cellView.model.attributes.nodeData?.id
        if (!nodeDataId) {
          return
        }

        this.props.actions.displayNodeDetails(this.getNodeData(cellView.model))

        if (evt.shiftKey || evt.ctrlKey || evt.metaKey) {
          this.props.actions.toggleSelection(nodeDataId)
        } else {
          this.props.actions.resetSelection(nodeDataId)
        }
      })
    }
  }

  hooverHandling() {
    this.processGraphPaper.on(Events.CELL_MOUSEOVER, (cellView) => {
      const model = cellView.model
      this.showLabelOnHover(model)
      this.showBackgroundIcon(model)
    })
    this.processGraphPaper.on(Events.BLANK_MOUSEOVER, () => {
      this.hideBackgroundsIcons()
    })
  }

  getNodeData(model) {
    const {processToDisplay} = this.props
    return NodeUtils.getNodeById(model.attributes.nodeData.id, processToDisplay)
  }

  //needed for proper switch/filter label handling
  showLabelOnHover(model) {
    if (!isBackgroundObject(model)) {
      model.toFront()
    }
    return model
  }

  //background is below normal node, we cannot use normal hover/mouseover/mouseout...
  showBackgroundIcon(model) {
    if (isBackgroundObject(model)) {
      const el = model.findView(this.processGraphPaper).vel
      el.toggleClass("forced-hover", true)
    }
  }

  hideBackgroundsIcons() {
    this.graph.getElements().filter(isBackgroundObject).forEach(model => {
      const el = model.findView(this.processGraphPaper).vel
      el.toggleClass("forced-hover", false)
    })
  }

  moveSelectedNodesRelatively(element, position) {
    this.redrawing = true
    const movedNodeId = element.id
    const nodeIdsToBeMoved = _.without(this.props.selectionState, movedNodeId)
    const cellsToBeMoved = nodeIdsToBeMoved.map(nodeId => this.graph.getCell(nodeId))
    const {position: originalPosition} = this.findNodeInLayout(movedNodeId)
    const offset = {x: position.x - originalPosition.x, y: position.y - originalPosition.y}
    cellsToBeMoved.filter(isModelElement).forEach(cell => {
      const {position: originalPosition} = this.findNodeInLayout(cell.id)
      cell.position(originalPosition.x + offset.x, originalPosition.y + offset.y)
    })
    this.redrawing = false
  }

  findNodeInLayout(nodeId) {
    return _.find(this.props.layout, n => n.id === nodeId)
  }

  nodesMoving() {
    this.graph.on(Events.CHANGE_POSITION, (element, position) => {
      if (!this.redrawing && this.props.selectionState?.includes(element.id) && isModelElement(element)) {
        this.moveSelectedNodesRelatively(element, position)
      }
    })
  }

  render() {
    const {connectDropTarget, divId, isSubprocess} = this.props
    return (
      <GraphPaperContainer
        ref={instance => {
          this.espGraphRef.current = instance
          if (connectDropTarget) {
            const node = findDOMNode(instance)
            connectDropTarget(node)
          }
        }}
        onResize={isSubprocess ? () => this.panAndZoom.fitSmallAndLargeGraphs() : null}
        id={divId}
      />
    )
  }
}

export function commonState(state) {
  return {
    layout: [],
    processCategory: getProcessCategory(state),
    loggedUser: getLoggedUser(state),
    processDefinitionData: getProcessDefinitionData(state),
    selectionState: getSelectionState(state),
  }
}
<commit_msg>fix: avoid unnecessary layout changes (#2181)<commit_after>/* eslint-disable i18next/no-literal-string */
import {dia} from "jointjs"
import "jointjs/dist/joint.css"
import _, {cloneDeep, debounce, isEqual, sortBy} from "lodash"
import PropTypes from "prop-types"
import React from "react"
import {findDOMNode} from "react-dom"
import {getProcessCategory, getSelectionState} from "../../reducers/selectors/graph"
import {getLoggedUser, getProcessDefinitionData} from "../../reducers/selectors/settings"
import "../../stylesheets/graph.styl"
import {filterDragHovered, setLinksHovered} from "./dragHelpers"
import {updateNodeCounts} from "./EspNode/element"
import {GraphPaperContainer} from "./focusable"
import {createPaper, directedLayout, drawGraph, isBackgroundObject, isModelElement} from "./GraphPartialsInTS"
import styles from "./graphTheme.styl"
import * as GraphUtils from "./GraphUtils"
import {Events} from "./joint-events"
import NodeUtils from "./NodeUtils"
import {PanZoomPlugin} from "./PanZoomPlugin"
import {RangeSelectPlugin, SelectionMode} from "./RangeSelectPlugin"
import "./svg-export/export.styl"
import {prepareSvg} from "./svg-export/prepareSvg"

export class Graph extends React.Component {

  static propTypes = {
    processToDisplay: PropTypes.object.isRequired,
    loggedUser: PropTypes.object.isRequired,
    connectDropTarget: PropTypes.func,
    showModalNodeDetails: PropTypes.func.isRequired,
    showModalEdgeDetails: PropTypes.func.isRequired,
    isSubprocess: PropTypes.bool,
  }
  redrawing = false
  directedLayout = directedLayout.bind(this)
  createPaper = createPaper.bind(this)
  drawGraph = drawGraph.bind(this)
  forceLayout = debounce(() => {
    this.directedLayout(this.props.selectionState)
    this.panAndZoom.fitSmallAndLargeGraphs()
  }, 50)

  constructor(props) {
    super(props)

    this.graph = new dia.Graph()
    this.graph.on(Events.REMOVE, (e, f) => {
      if (e.isLink() && !this.redrawing) {
        this.props.actions.nodesDisconnected(e.attributes.source.id, e.attributes.target.id)
      }
    })
    this.nodesMoving()

    this.espGraphRef = React.createRef()
  }

  getEspGraphRef = () => {
    return this.espGraphRef.current
  }

  componentWillUnmount() {
    // force destroy event on model for plugins cleanup
    this.processGraphPaper.model.destroy()
    this.unbindEventHandlers()
  }

  bindEventHandlers() {
    this.changeNodeDetailsOnClick()

    this.processGraphPaper.on(Events.BLANK_POINTERUP, event => {
      if (!event.isPropagationStopped()) {
        if (this.props.fetchedProcessDetails != null) {
          this.props.actions.displayNodeDetails(this.props.fetchedProcessDetails.json.properties)
          this.props.actions.resetSelection()
        }
      }
    })
    this.hooverHandling()
  }

  unbindEventHandlers() {

  }

  componentDidMount() {
    this.processGraphPaper = this.createPaper()
    this.processGraphPaper.freeze()
    this.drawGraph(
      this.props.processToDisplay,
      this.props.layout,
      this.props.processDefinitionData,
    )
    this.processGraphPaper.unfreeze()
    this._prepareContentForExport()

    // event handlers binding below. order sometimes matters
    this.panAndZoom = new PanZoomPlugin(this.processGraphPaper)
    new RangeSelectPlugin(this.processGraphPaper)
    this.processGraphPaper.on("rangeSelect:selected", ({elements, mode}) => {
      const nodes = elements
        .filter(el => isModelElement(el))
        .map(({id}) => id)
      if (mode === SelectionMode.toggle) {
        this.props.actions.toggleSelection(...nodes)
      } else {
        this.props.actions.resetSelection(...nodes)
      }
    })

    this.bindEventHandlers()
    this.highlightNodes(this.props.processToDisplay, this.props.nodeToDisplay)
    this.updateNodesCounts()

    this.graph.on(Events.CHANGE_DRAG_OVER, () => {
      const links = this.graph.getLinks()
      links.forEach(l => this.unhighlightCell(l, styles.dragHovered))

      const [active] = filterDragHovered(links)

      if (active) {
        this.highlightCell(active, styles.dragHovered)
        active.toBack()
      }
    })

    //we want to inject node during 'Drag and Drop' from graph paper
    this.graph.on(Events.ADD, (cell) => {
      if (this.isNotLink(cell)) {
        this.handleInjectBetweenNodes(cell)
        setLinksHovered(this.graph)
      }
    })

    this.panAndZoom.fitSmallAndLargeGraphs()
  }

  isNotLink(cell) {
    return cell.attributes.type !== "link"
  }

  canAddNode(node) {
    return this.props.capabilities.write &&
      NodeUtils.isNode(node) &&
      NodeUtils.isAvailable(node, this.props.processDefinitionData, this.props.processCategory)
  }

  addNode(node, position) {
    if (this.canAddNode(node)) {
      this.props.actions.nodeAdded(node, position)
    }
  }

  componentWillUpdate(nextProps, nextState) {
    const processChanged = !isEqual(this.props.processToDisplay, nextProps.processToDisplay) ||
      !isEqual(this.props.layout, nextProps.layout) ||
      !isEqual(this.props.processDefinitionData, nextProps.processDefinitionData)
    if (processChanged) {
      this.drawGraph(
        nextProps.processToDisplay,
        nextProps.layout,
        nextProps.processDefinitionData,
      )
    }

    //when e.g. layout changed we have to remember to highlight nodes
    const nodeToDisplayChanged = !isEqual(this.props.nodeToDisplay, nextProps.nodeToDisplay)
    const selectedNodesChanged = !isEqual(this.props.selectionState, nextProps.selectionState)
    if (processChanged || nodeToDisplayChanged || selectedNodesChanged) {
      this.highlightNodes(nextProps.processToDisplay, nextProps.nodeToDisplay, nextProps.selectionState)
    }
  }

  componentDidUpdate(prevProps, prevState) {
    const {processCounts} = this.props
    if (!isEqual(processCounts, prevProps.processCounts)) {
      this.updateNodesCounts()
    }
  }

  updateNodesCounts() {
    const {processCounts} = this.props
    const nodes = this.graph.getElements().filter(isModelElement)
    nodes.forEach(updateNodeCounts(processCounts))
  }

  zoomIn() {
    this.panAndZoom.zoomIn()
  }

  zoomOut() {
    this.panAndZoom.zoomOut()
  }

  async exportGraph() {
    return await prepareSvg(this._exportGraphOptions)
  }

  validateConnection = (cellViewS, magnetS, cellViewT, magnetT, end, linkView) => {
    const from = cellViewS.model.id
    const to = cellViewT.model.id
    const previousEdge = linkView.model.attributes.edgeData || {}
    return magnetT && NodeUtils.canMakeLink(from, to, this.props.processToDisplay, this.props.processDefinitionData, previousEdge)
  }

  disconnectPreviousEdge = (from, to) => {
    if (this.graphContainsEdge(from, to)) {
      this.props.actions.nodesDisconnected(from, to)
    }
  }

  graphContainsEdge(from, to) {
    return this.props.processToDisplay.edges.some(edge => edge.from === from && edge.to === to)
  }

  handleInjectBetweenNodes = (middleMan) => {
    const links = this.graph.getLinks()
    const [linkBelowCell] = filterDragHovered(links)

    if (linkBelowCell && middleMan) {
      const source = this.graph.getCell(linkBelowCell.getSourceElement().id)
      const target = this.graph.getCell(linkBelowCell.getTargetElement().id)

      const middleManNode = middleMan.attributes.nodeData

      const sourceNode = source.attributes.nodeData
      const targetNode = target.attributes.nodeData

      if (GraphUtils.canInjectNode(
        this.props.processToDisplay,
        sourceNode.id,
        middleMan.id,
        targetNode.id,
        this.props.processDefinitionData,
      )) {
        //TODO: consider doing inject check in actions.js?
        this.props.actions.injectNode(
          sourceNode,
          middleManNode,
          targetNode,
          linkBelowCell.attributes.edgeData.edgeType,
        )
      }
    }
  }

  _prepareContentForExport = () => {
    const {options, defs} = this.processGraphPaper
    this._exportGraphOptions = {
      options: cloneDeep(options),
      defs: defs.cloneNode(true),
    }
  }

  highlightNodes = (data, nodeToDisplay, selectionState) => {
    this.graph.getCells().forEach(cell => {
      this.unhighlightCell(cell, "node-validation-error")
      this.unhighlightCell(cell, "node-focused")
      this.unhighlightCell(cell, "node-focused-with-validation-error")
    })

    const invalidNodeIds = _.keys((data.validationResult && data.validationResult.errors || {}).invalidNodes)
    const selectedNodeIds = selectionState || []

    invalidNodeIds.forEach(id => selectedNodeIds.includes(id) ?
      this.highlightNode(id, "node-focused-with-validation-error") :
      this.highlightNode(id, "node-validation-error"))

    selectedNodeIds.forEach(id => {
      if (!invalidNodeIds.includes(id)) {
        this.highlightNode(id, "node-focused")
      }
    })
  }

  highlightCell(cell, className) {
    this.processGraphPaper.findViewByModel(cell).highlight(null, {
      highlighter: {
        name: "addClass",
        options: {className: className},
      },
    })
  }

  unhighlightCell(cell, className) {
    this.processGraphPaper.findViewByModel(cell).unhighlight(null, {
      highlighter: {
        name: "addClass",
        options: {className: className},
      },
    })
  }

  highlightNode = (nodeId, highlightClass) => {
    const cell = this.graph.getCell(nodeId)
    if (cell) { //prevent `properties` node highlighting
      this.highlightCell(cell, highlightClass)
    }
  }

  changeLayoutIfNeeded = () => {
    const {layout, actions, isSubprocess} = this.props

    if (isSubprocess) {
      return
    }

    const elements = this.graph.getElements().filter(isModelElement)
    const collection = elements.map(el => {
      const {x, y} = el.get("position")
      return {id: el.id, position: {x, y}}
    })

    const iteratee = e => e.id
    const newLayout = sortBy(collection, iteratee)
    const oldLayout = sortBy(layout, iteratee)

    if (!isEqual(oldLayout, newLayout)) {
      actions?.layoutChanged(newLayout)
    }
  }

  changeNodeDetailsOnClick() {
    this.processGraphPaper.on(Events.CELL_POINTERDBLCLICK, (cellView) => {
      const nodeDataId = cellView.model.attributes.nodeData?.id
      if (nodeDataId) {
        const nodeData = this.getNodeData(cellView.model)
        const prefixedNodeId = this.props.nodeIdPrefixForSubprocessTests + nodeDataId
        this.props.showModalNodeDetails({...nodeData, id: prefixedNodeId}, this.props.readonly)
      }

      if (cellView.model.attributes.edgeData) {
        this.props.showModalEdgeDetails(cellView.model.attributes.edgeData)
      }
    })

    if (this.props.singleClickNodeDetailsEnabled) {
      this.processGraphPaper.on(Events.CELL_POINTERCLICK, (cellView, evt) => {

        const nodeDataId = cellView.model.attributes.nodeData?.id
        if (!nodeDataId) {
          return
        }

        this.props.actions.displayNodeDetails(this.getNodeData(cellView.model))

        if (evt.shiftKey || evt.ctrlKey || evt.metaKey) {
          this.props.actions.toggleSelection(nodeDataId)
        } else {
          this.props.actions.resetSelection(nodeDataId)
        }
      })
    }
  }

  hooverHandling() {
    this.processGraphPaper.on(Events.CELL_MOUSEOVER, (cellView) => {
      const model = cellView.model
      this.showLabelOnHover(model)
      this.showBackgroundIcon(model)
    })
    this.processGraphPaper.on(Events.BLANK_MOUSEOVER, () => {
      this.hideBackgroundsIcons()
    })
  }

  getNodeData(model) {
    const {processToDisplay} = this.props
    return NodeUtils.getNodeById(model.attributes.nodeData.id, processToDisplay)
  }

  //needed for proper switch/filter label handling
  showLabelOnHover(model) {
    if (!isBackgroundObject(model)) {
      model.toFront()
    }
    return model
  }

  //background is below normal node, we cannot use normal hover/mouseover/mouseout...
  showBackgroundIcon(model) {
    if (isBackgroundObject(model)) {
      const el = model.findView(this.processGraphPaper).vel
      el.toggleClass("forced-hover", true)
    }
  }

  hideBackgroundsIcons() {
    this.graph.getElements().filter(isBackgroundObject).forEach(model => {
      const el = model.findView(this.processGraphPaper).vel
      el.toggleClass("forced-hover", false)
    })
  }

  moveSelectedNodesRelatively(element, position) {
    this.redrawing = true
    const movedNodeId = element.id
    const nodeIdsToBeMoved = _.without(this.props.selectionState, movedNodeId)
    const cellsToBeMoved = nodeIdsToBeMoved.map(nodeId => this.graph.getCell(nodeId))
    const {position: originalPosition} = this.findNodeInLayout(movedNodeId)
    const offset = {x: position.x - originalPosition.x, y: position.y - originalPosition.y}
    cellsToBeMoved.filter(isModelElement).forEach(cell => {
      const {position: originalPosition} = this.findNodeInLayout(cell.id)
      cell.position(originalPosition.x + offset.x, originalPosition.y + offset.y)
    })
    this.redrawing = false
  }

  findNodeInLayout(nodeId) {
    return _.find(this.props.layout, n => n.id === nodeId)
  }

  nodesMoving() {
    this.graph.on(Events.CHANGE_POSITION, (element, position) => {
      if (!this.redrawing && this.props.selectionState?.includes(element.id) && isModelElement(element)) {
        this.moveSelectedNodesRelatively(element, position)
      }
    })
  }

  render() {
    const {connectDropTarget, divId, isSubprocess} = this.props
    return (
      <GraphPaperContainer
        ref={instance => {
          this.espGraphRef.current = instance
          if (connectDropTarget) {
            const node = findDOMNode(instance)
            connectDropTarget(node)
          }
        }}
        onResize={isSubprocess ? () => this.panAndZoom.fitSmallAndLargeGraphs() : null}
        id={divId}
      />
    )
  }
}

export function commonState(state) {
  return {
    layout: [],
    processCategory: getProcessCategory(state),
    loggedUser: getLoggedUser(state),
    processDefinitionData: getProcessDefinitionData(state),
    selectionState: getSelectionState(state),
  }
}
<|endoftext|>


<commit_before>const EventEmitter = require('events')
const Long = require('long')
const createRetry = require('../retry')
const limitConcurrency = require('../utils/concurrency')
const { KafkaJSError } = require('../errors')
const barrier = require('./barrier')

const {
  events: { GROUP_JOIN, FETCH, FETCH_START, START_BATCH_PROCESS, END_BATCH_PROCESS },
} = require('./instrumentationEvents')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError
const isSameOffset = (offsetA, offsetB) => Long.fromValue(offsetA).equals(Long.fromValue(offsetB))
const CONSUMING_START = 'consuming-start'
const CONSUMING_STOP = 'consuming-stop'

module.exports = class Runner extends EventEmitter {
  constructor({
    logger,
    consumerGroup,
    instrumentationEmitter,
    eachBatchAutoResolve = true,
    partitionsConsumedConcurrently,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
    autoCommit = true,
  }) {
    super()
    this.logger = logger.namespace('Runner')
    this.consumerGroup = consumerGroup
    this.instrumentationEmitter = instrumentationEmitter
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash
    this.autoCommit = autoCommit
    this.partitionsConsumedConcurrently = partitionsConsumedConcurrently

    this.running = false
    this.consuming = false
  }

  get consuming() {
    return this._consuming
  }

  set consuming(value) {
    if (this._consuming !== value) {
      this._consuming = value
      this.emit(value ? CONSUMING_START : CONSUMING_STOP)
    }
  }

  async join() {
    const startJoin = Date.now()
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()

        this.running = true

        const memberAssignment = this.consumerGroup
          .assigned()
          .reduce((result, { topic, partitions }) => ({ ...result, [topic]: partitions }), {})

        const payload = {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
          isLeader: this.consumerGroup.isLeader(),
          memberAssignment,
          groupProtocol: this.consumerGroup.groupProtocol,
          duration: Date.now() - startJoin,
        }

        this.instrumentationEmitter.emit(GROUP_JOIN, payload)
        this.logger.info('Consumer has joined the group', payload)
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async scheduleJoin() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.join().catch(this.onCrash)
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.consumerGroup.connect()
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    if (!this.running) {
      return
    }

    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })

    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      if (!this.consuming) {
        return resolve()
      }
      this.logger.debug('waiting for consumer to finish...', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      this.once(CONSUMING_STOP, () => resolve())
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (const message of batch.messages) {
      if (!this.running || this.consumerGroup.hasSeekOffset({ topic, partition })) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
      await this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch
    const lastFilteredMessage = batch.messages[batch.messages.length - 1]

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          /**
           * The transactional producer generates a control record after committing the transaction.
           * The control record is the last record on the RecordBatch, and it is filtered before it
           * reaches the eachBatch callback. When disabling auto-resolve, the user-land code won't
           * be able to resolve the control record offset, since it never reaches the callback,
           * causing stuck consumers as the consumer will never move the offset marker.
           *
           * When the last offset of the batch is resolved, we should automatically resolve
           * the control record offset as this entry doesn't have any meaning to the user-land code,
           * and won't interfere with the stream processing.
           *
           * @see https://github.com/apache/kafka/blob/9aa660786e46c1efbf5605a6a69136a1dac6edb9/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java#L1499-L1505
           */
          const offsetToResolve =
            lastFilteredMessage && isSameOffset(offset, lastFilteredMessage.offset)
              ? batch.lastOffset()
              : offset

          this.consumerGroup.resolveOffset({ topic, partition, offset: offsetToResolve })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        /**
         * Commit offsets if provided. Otherwise commit most recent resolved offsets
         * if the autoCommit conditions are met.
         *
         * @param {OffsetsByTopicPartition} [offsets] Optional.
         */
        commitOffsetsIfNecessary: async offsets => {
          return offsets
            ? this.consumerGroup.commitOffsets(offsets)
            : this.consumerGroup.commitOffsetsIfNecessary()
        },
        uncommittedOffsets: () => this.consumerGroup.uncommittedOffsets(),
        isRunning: () => this.running,
        isStale: () => this.consumerGroup.hasSeekOffset({ topic, partition }),
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.autoCommitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const startFetch = Date.now()

    this.instrumentationEmitter.emit(FETCH_START, {})

    const iterator = await this.consumerGroup.fetch()

    this.instrumentationEmitter.emit(FETCH, {
      /**
       * PR #570 removed support for the number of batches in this instrumentation event;
       * The new implementation uses an async generation to deliver the batches, which makes
       * this number impossible to get. The number is set to 0 to keep the event backward
       * compatible until we bump KafkaJS to version 2, following the end of node 8 LTS.
       *
       * @since 2019-11-29
       */
      numberOfBatches: 0,
      duration: Date.now() - startFetch,
    })

    const onBatch = async batch => {
      const startBatchProcess = Date.now()
      const payload = {
        topic: batch.topic,
        partition: batch.partition,
        highWatermark: batch.highWatermark,
        offsetLag: batch.offsetLag(),
        /**
         * @since 2019-06-24 (>= 1.8.0)
         *
         * offsetLag returns the lag based on the latest offset in the batch, to
         * keep the event backward compatible we just introduced "offsetLagLow"
         * which calculates the lag based on the first offset in the batch
         */
        offsetLagLow: batch.offsetLagLow(),
        batchSize: batch.messages.length,
        firstOffset: batch.firstOffset(),
        lastOffset: batch.lastOffset(),
      }

      this.instrumentationEmitter.emit(START_BATCH_PROCESS, payload)

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }

      this.instrumentationEmitter.emit(END_BATCH_PROCESS, {
        ...payload,
        duration: Date.now() - startBatchProcess,
      })
    }

    const { lock, unlock, unlockWithError } = barrier()
    const concurrently = limitConcurrency({ limit: this.partitionsConsumedConcurrently })

    let requestsCompleted = false
    let numberOfExecutions = 0
    let expectedNumberOfExecutions = 0
    const enqueuedTasks = []

    while (true) {
      const result = iterator.next()
      if (result.done) {
        break
      }

      enqueuedTasks.push(async () => {
        const batches = await result.value
        expectedNumberOfExecutions += batches.length

        if (!this.running) {
          return
        }

        batches.map(batch =>
          concurrently(async () => {
            try {
              if (!this.running) {
                return
              }

              if (batch.isEmpty()) {
                return
              }

              await onBatch(batch)
              await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
            } catch (e) {
              unlockWithError(e)
            } finally {
              numberOfExecutions++
              if (requestsCompleted && numberOfExecutions === expectedNumberOfExecutions) {
                unlock()
              }
            }
          }).catch(unlockWithError)
        )
      })
    }

    await Promise.all(enqueuedTasks.map(fn => fn()))
    requestsCompleted = true

    if (expectedNumberOfExecutions === numberOfExecutions) {
      unlock()
    }

    const error = await lock
    if (error) {
      throw error
    }

    if (!this.running) {
      return
    }

    await this.autoCommitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }

  autoCommitOffsets() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsets()
    }
  }

  autoCommitOffsetsIfNecessary() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  commitOffsets(offsets) {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
        offsets,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.commitOffsets(offsets)
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            offsets,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while committing offsets, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
          offsets,
        })

        throw e
      }
    })
  }
}
<commit_msg>Remove not necessary running check<commit_after>const EventEmitter = require('events')
const Long = require('long')
const createRetry = require('../retry')
const limitConcurrency = require('../utils/concurrency')
const { KafkaJSError } = require('../errors')
const barrier = require('./barrier')

const {
  events: { GROUP_JOIN, FETCH, FETCH_START, START_BATCH_PROCESS, END_BATCH_PROCESS },
} = require('./instrumentationEvents')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError
const isSameOffset = (offsetA, offsetB) => Long.fromValue(offsetA).equals(Long.fromValue(offsetB))
const CONSUMING_START = 'consuming-start'
const CONSUMING_STOP = 'consuming-stop'

module.exports = class Runner extends EventEmitter {
  constructor({
    logger,
    consumerGroup,
    instrumentationEmitter,
    eachBatchAutoResolve = true,
    partitionsConsumedConcurrently,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
    autoCommit = true,
  }) {
    super()
    this.logger = logger.namespace('Runner')
    this.consumerGroup = consumerGroup
    this.instrumentationEmitter = instrumentationEmitter
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash
    this.autoCommit = autoCommit
    this.partitionsConsumedConcurrently = partitionsConsumedConcurrently

    this.running = false
    this.consuming = false
  }

  get consuming() {
    return this._consuming
  }

  set consuming(value) {
    if (this._consuming !== value) {
      this._consuming = value
      this.emit(value ? CONSUMING_START : CONSUMING_STOP)
    }
  }

  async join() {
    const startJoin = Date.now()
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()

        this.running = true

        const memberAssignment = this.consumerGroup
          .assigned()
          .reduce((result, { topic, partitions }) => ({ ...result, [topic]: partitions }), {})

        const payload = {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
          isLeader: this.consumerGroup.isLeader(),
          memberAssignment,
          groupProtocol: this.consumerGroup.groupProtocol,
          duration: Date.now() - startJoin,
        }

        this.instrumentationEmitter.emit(GROUP_JOIN, payload)
        this.logger.info('Consumer has joined the group', payload)
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async scheduleJoin() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.join().catch(this.onCrash)
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.consumerGroup.connect()
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    if (!this.running) {
      return
    }

    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })

    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      if (!this.consuming) {
        return resolve()
      }
      this.logger.debug('waiting for consumer to finish...', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      this.once(CONSUMING_STOP, () => resolve())
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (const message of batch.messages) {
      if (!this.running || this.consumerGroup.hasSeekOffset({ topic, partition })) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
      await this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch
    const lastFilteredMessage = batch.messages[batch.messages.length - 1]

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          /**
           * The transactional producer generates a control record after committing the transaction.
           * The control record is the last record on the RecordBatch, and it is filtered before it
           * reaches the eachBatch callback. When disabling auto-resolve, the user-land code won't
           * be able to resolve the control record offset, since it never reaches the callback,
           * causing stuck consumers as the consumer will never move the offset marker.
           *
           * When the last offset of the batch is resolved, we should automatically resolve
           * the control record offset as this entry doesn't have any meaning to the user-land code,
           * and won't interfere with the stream processing.
           *
           * @see https://github.com/apache/kafka/blob/9aa660786e46c1efbf5605a6a69136a1dac6edb9/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java#L1499-L1505
           */
          const offsetToResolve =
            lastFilteredMessage && isSameOffset(offset, lastFilteredMessage.offset)
              ? batch.lastOffset()
              : offset

          this.consumerGroup.resolveOffset({ topic, partition, offset: offsetToResolve })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        /**
         * Commit offsets if provided. Otherwise commit most recent resolved offsets
         * if the autoCommit conditions are met.
         *
         * @param {OffsetsByTopicPartition} [offsets] Optional.
         */
        commitOffsetsIfNecessary: async offsets => {
          return offsets
            ? this.consumerGroup.commitOffsets(offsets)
            : this.consumerGroup.commitOffsetsIfNecessary()
        },
        uncommittedOffsets: () => this.consumerGroup.uncommittedOffsets(),
        isRunning: () => this.running,
        isStale: () => this.consumerGroup.hasSeekOffset({ topic, partition }),
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.autoCommitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const startFetch = Date.now()

    this.instrumentationEmitter.emit(FETCH_START, {})

    const iterator = await this.consumerGroup.fetch()

    this.instrumentationEmitter.emit(FETCH, {
      /**
       * PR #570 removed support for the number of batches in this instrumentation event;
       * The new implementation uses an async generation to deliver the batches, which makes
       * this number impossible to get. The number is set to 0 to keep the event backward
       * compatible until we bump KafkaJS to version 2, following the end of node 8 LTS.
       *
       * @since 2019-11-29
       */
      numberOfBatches: 0,
      duration: Date.now() - startFetch,
    })

    const onBatch = async batch => {
      const startBatchProcess = Date.now()
      const payload = {
        topic: batch.topic,
        partition: batch.partition,
        highWatermark: batch.highWatermark,
        offsetLag: batch.offsetLag(),
        /**
         * @since 2019-06-24 (>= 1.8.0)
         *
         * offsetLag returns the lag based on the latest offset in the batch, to
         * keep the event backward compatible we just introduced "offsetLagLow"
         * which calculates the lag based on the first offset in the batch
         */
        offsetLagLow: batch.offsetLagLow(),
        batchSize: batch.messages.length,
        firstOffset: batch.firstOffset(),
        lastOffset: batch.lastOffset(),
      }

      this.instrumentationEmitter.emit(START_BATCH_PROCESS, payload)

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }

      this.instrumentationEmitter.emit(END_BATCH_PROCESS, {
        ...payload,
        duration: Date.now() - startBatchProcess,
      })
    }

    const { lock, unlock, unlockWithError } = barrier()
    const concurrently = limitConcurrency({ limit: this.partitionsConsumedConcurrently })

    let requestsCompleted = false
    let numberOfExecutions = 0
    let expectedNumberOfExecutions = 0
    const enqueuedTasks = []

    while (true) {
      const result = iterator.next()
      if (result.done) {
        break
      }

      enqueuedTasks.push(async () => {
        const batches = await result.value
        expectedNumberOfExecutions += batches.length

        batches.map(batch =>
          concurrently(async () => {
            try {
              if (!this.running) {
                return
              }

              if (batch.isEmpty()) {
                return
              }

              await onBatch(batch)
              await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
            } catch (e) {
              unlockWithError(e)
            } finally {
              numberOfExecutions++
              if (requestsCompleted && numberOfExecutions === expectedNumberOfExecutions) {
                unlock()
              }
            }
          }).catch(unlockWithError)
        )
      })
    }

    await Promise.all(enqueuedTasks.map(fn => fn()))
    requestsCompleted = true

    if (expectedNumberOfExecutions === numberOfExecutions) {
      unlock()
    }

    const error = await lock
    if (error) {
      throw error
    }

    if (!this.running) {
      return
    }

    await this.autoCommitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }

  autoCommitOffsets() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsets()
    }
  }

  autoCommitOffsetsIfNecessary() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  commitOffsets(offsets) {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
        offsets,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.commitOffsets(offsets)
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            offsets,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while committing offsets, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
          offsets,
        })

        throw e
      }
    })
  }
}
<|endoftext|>


<commit_before>module.exports = function() {
    const gulp = require('gulp');
    const plumber = require('gulp-plumber');
    const notify = require('gulp-notify');
    const gulpif = require('gulp-if');
    const browserSync = require('browser-sync');
    const util = require('gulp-util');

    const config = util.env.boilerplate.config;
    const sassPipe = require('../pipe/sass');

    const sassConfig = config.tasks.sass;

    const isWatching = ['serve', 'watch'].indexOf(process.argv[2]) >= 0;

    return gulp.src(sassConfig.source, {cwd: config.sourceRoot})
    .pipe(plumber({
        errorHandler: notify.onError({
            message: '<%= error.message %>',
            title: 'SASS Error'
        })
    }))
    .pipe(sassPipe())
    .pipe(gulp.dest(config.destinationRoot + sassConfig.destination))
    .pipe(gulpif(isWatching, browserSync.stream({once: true})))
    .pipe(notify({ message: 'Successfully compiled SASS', onLast: true }))
    .on('error', function() {
        this.emit('error', new Error('SASS compilation Error'));
    });
};
<commit_msg>[fix] BOILERPLATE-54: Fixed sass browsersnc reloading<commit_after>module.exports = function() {
    const gulp = require('gulp');
    const plumber = require('gulp-plumber');
    const notify = require('gulp-notify');
    const gulpif = require('gulp-if');
    const browserSync = require('browser-sync');
    const util = require('gulp-util');

    const config = util.env.boilerplate.config;
    const sassPipe = require('../pipe/sass');

    const sassConfig = config.tasks.sass;

    const isWatching = ['serve', 'watch'].indexOf(process.argv[2]) >= 0;

    return gulp.src(sassConfig.source, {cwd: config.sourceRoot})
    .pipe(plumber({
        errorHandler: notify.onError({
            message: '<%= error.message %>',
            title: 'SASS Error'
        })
    }))
    .pipe(sassPipe())
    .pipe(gulp.dest(config.destinationRoot + sassConfig.destination))
    .pipe(gulpif(isWatching, browserSync.stream()))
    .pipe(notify({ message: 'Successfully compiled SASS', onLast: true }))
    .on('error', function() {
        this.emit('error', new Error('SASS compilation Error'));
    });
};
<|endoftext|>