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

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

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 {
  /**
   * @param {object} options
   * @param {import("../../types").Logger} options.logger
   * @param {import("./consumerGroup")} options.consumerGroup
   * @param {import("../instrumentation/emitter")} options.instrumentationEmitter
   * @param {boolean} [options.eachBatchAutoResolve=true]
   * @param {number} [options.partitionsConsumedConcurrently]
   * @param {(payload: import("../../types").EachBatchPayload) => Promise<void>} options.eachBatch
   * @param {(payload: import("../../types").EachMessagePayload) => Promise<void>} options.eachMessage
   * @param {number} [options.heartbeatInterval]
   * @param {(reason: Error) => void} options.onCrash
   * @param {import("../../types").RetryOptions} [options.retry]
   * @param {boolean} [options.autoCommit=true]
   */
  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() {
    await this.consumerGroup.joinAndSync()
    this.running = true
  }

  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 {
      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,
          heartbeat: async () => {
            await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
          },
        })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
            error: e,
          })
        }

        // In case of errors, commit the previously consumed offsets unless autoCommit is disabled
        await this.autoCommitOffsets()
        throw e
      }

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

  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,
          error: e,
        })
      }

      // 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
      }

      if (!this.running) {
        result.value.catch(error => {
          this.logger.debug('Ignoring error in fetch request while stopping runner', {
            error: error.message || error,
            stack: error.stack,
          })
        })

        continue
      }

      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
    }

    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

        if (this.running) {
          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,
          })

          this.instrumentationEmitter.emit(REBALANCING, {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })

          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,
          })

          this.instrumentationEmitter.emit(REBALANCING, {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })

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

          bail(new KafkaJSError(e))
        }

        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(e))
        }

        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>log warn instead of error when rebalancing<commit_after>const { EventEmitter } = require('events')
const Long = require('../utils/long')
const createRetry = require('../retry')
const limitConcurrency = require('../utils/concurrency')
const { KafkaJSError } = require('../errors')
const barrier = require('./barrier')

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

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 {
  /**
   * @param {object} options
   * @param {import("../../types").Logger} options.logger
   * @param {import("./consumerGroup")} options.consumerGroup
   * @param {import("../instrumentation/emitter")} options.instrumentationEmitter
   * @param {boolean} [options.eachBatchAutoResolve=true]
   * @param {number} [options.partitionsConsumedConcurrently]
   * @param {(payload: import("../../types").EachBatchPayload) => Promise<void>} options.eachBatch
   * @param {(payload: import("../../types").EachMessagePayload) => Promise<void>} options.eachMessage
   * @param {number} [options.heartbeatInterval]
   * @param {(reason: Error) => void} options.onCrash
   * @param {import("../../types").RetryOptions} [options.retry]
   * @param {boolean} [options.autoCommit=true]
   */
  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() {
    await this.consumerGroup.joinAndSync()
    this.running = true
  }

  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 {
      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,
          heartbeat: async () => {
            await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
          },
        })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
            error: e,
          })
        }

        // In case of errors, commit the previously consumed offsets unless autoCommit is disabled
        await this.autoCommitOffsets()
        throw e
      }

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

  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,
          error: e,
        })
      }

      // 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
      }

      if (!this.running) {
        result.value.catch(error => {
          this.logger.debug('Ignoring error in fetch request while stopping runner', {
            error: error.message || error,
            stack: error.stack,
          })
        })

        continue
      }

      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
    }

    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

        if (this.running) {
          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.warn('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.instrumentationEmitter.emit(REBALANCING, {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })

          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.warn('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.instrumentationEmitter.emit(REBALANCING, {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })

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

          bail(new KafkaJSError(e))
        }

        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(e))
        }

        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>(function(jQuery) {
"use strict";

var $ = jQuery;

/**
 * @class Echo.StreamServer.Controls.Stream.Plugins.InfiniteScroll
 * Echo Stream plugin automatically loads the next page full of items
 * when the end of the stream is displayed in the browser.
 * This produces the 'Infinite Scroll' Effect.
 *
 * 	new Echo.StreamServer.Controls.Stream({
 * 		"target": document.getElementById("echo-stream"),
 * 		"appkey": "echo.jssdk.demo.aboutecho.com",
 * 		"plugins": [{
 * 			"name": "InfiniteScroll"
 * 		}]
 * 	});
 *
 * More information regarding the plugins installation can be found
 * in the ["How to initialize Echo components"](#!/guide/how_to_initialize_components-section-initializing-plugins) guide.
 *
 * @extends Echo.Plugin
 *
 * @package streamserver/plugins.pack.js
 * @package streamserver.pack.js
 */
var plugin = Echo.Plugin.manifest("InfiniteScroll", "Echo.StreamServer.Controls.Stream");

if (Echo.Plugin.isDefined(plugin)) return;

plugin.init = function() {
	var plugin = this;
	var handler = function(event) {
		var element = plugin.component.view.get("more");
		if (element && !element.data("clicked") &&
				!plugin.get("requestInProgress") &&
				$.inviewport(element, {"threshold": 0})
		) {
				plugin.set("requestInProgress", true);
				element.data("clicked", true).click();
		}
	};
	plugin.set("scrollHandler", handler);
	$(window).on("scroll", handler);
};

plugin.events = {
	"Echo.StreamServer.Controls.Stream.onDataReceive": function(topic, args) {
		this.set("requestInProgress", false);
	}
};

plugin.component.renderers.more = function(element) {
	this.parentRenderer("more", arguments);
	return element.data("clicked", false);
};

plugin.methods.destroy = function() {
	$(window).off("scroll", this.get("scrollHandler"));
};

Echo.Plugin.create(plugin);

})(Echo.jQuery);
<commit_msg>Infinite-Scroll plugin code improvements<commit_after>(function(jQuery) {
"use strict";

var $ = jQuery;

/**
 * @class Echo.StreamServer.Controls.Stream.Plugins.InfiniteScroll
 * Echo Stream plugin automatically loads the next page full of items
 * when the end of the stream is displayed in the browser.
 * This produces the 'Infinite Scroll' Effect.
 *
 * 	new Echo.StreamServer.Controls.Stream({
 * 		"target": document.getElementById("echo-stream"),
 * 		"appkey": "echo.jssdk.demo.aboutecho.com",
 * 		"plugins": [{
 * 			"name": "InfiniteScroll"
 * 		}]
 * 	});
 *
 * More information regarding the plugins installation can be found
 * in the ["How to initialize Echo components"](#!/guide/how_to_initialize_components-section-initializing-plugins) guide.
 *
 * @extends Echo.Plugin
 *
 * @package streamserver/plugins.pack.js
 * @package streamserver.pack.js
 */
var plugin = Echo.Plugin.manifest("InfiniteScroll", "Echo.StreamServer.Controls.Stream");

if (Echo.Plugin.isDefined(plugin)) return;

plugin.init = function() {
	var plugin = this;
	var handler = function(event) {
		var element = plugin.component.view.get("more");
		if (element &&
			!plugin.get("renderInProgress") &&
			!plugin.get("requestInProgress") &&
			$.inviewport(element, {"threshold": 0})
		) {
			element.click();
		}
	};
	plugin.set("scrollHandler", handler);
	$(window).on("scroll", handler);
};

plugin.events = {
	"Echo.StreamServer.Controls.Stream.onDataReceive": function() {
		this.set("requestInProgress", false);
	},
	"Echo.StreamServer.Controls.Stream.onMoreButtonPress": function() {
		this.set("requestInProgress", true);
		this.set("renderInProgress", true);
	}
};

plugin.component.renderers.more = function(element) {
	this.parentRenderer("more", arguments);
	this.set("renderInProgress", false);
	return element;
};

plugin.methods.destroy = function() {
	$(window).off("scroll", this.get("scrollHandler"));
};

Echo.Plugin.create(plugin);

})(Echo.jQuery);
<|endoftext|>


<commit_before>"use strict";

import BaseComponent from 'generic/baseComponent';
import MediatorEvents from '../enums/mediatorEvents';
import MediatorEventModel from '../models/mediatorEventModel';

export default class TabsControl extends BaseComponent {
    constructor() {
        super( 'map-tabs' );
        this.prefix = 'mt';
        //this._elem = document.querySelector( '.js-tabs' );
        this._tabs = {};
        this._currentTabName = '';

        this._mediator = null;

        this.init();
        console.log( this );
    }

    init() {

        let navItemNodes = this.rootNode.querySelectorAll( '[data-mt-elem=nav-list] > [data-mt-elem=nav-item]' );
        navItemNodes = Array.prototype.slice.call( navItemNodes );

        navItemNodes.forEach( ( navItemNode ) => {
            let tabName = navItemNode.getAttribute( 'data-mt-name' ),
                tabNode = this.rootNode.querySelector(' [data-mt-elem=tab-item][data-mt-name=' + tabName + ']');
            //querySelector( '.js-tab[data-name=' + tabName + ']' );

            this._tabs[tabName] = {
                tabNavNode: navItemNode,
                tabNode: tabNode,
                contentNode: tabNode.querySelector( '[data-mt-elem=tab-container][data-mt-name=content]' ),
                loadmoreNode: tabNode.querySelector( '[data-mt-elem=tab-container][data-mt-name=loadmore]' ),
                detailsNode: this.rootNode.querySelector( '[data-mt-elem=tab-details]' )
            };

            navItemNode.addEventListener( 'click', event => this._clickHandler( event ) );

            //TODO: refactoring this with delegate
            if ( this._tabs[tabName].loadmoreNode ) {
                this._tabs[tabName].loadmoreNode.addEventListener( 'click', event => this._onClickLoadmoreHandler( event ) );
            }

        } );

        this._btnHideDetails = this.rootNode.querySelector( '[data-mt-elem=hide-details]' );

        if ( this._btnHideDetails ) {
            this._btnHideDetails.addEventListener(
                'click',
                event => this._onClickBtnHideDetailsHandler( event )
            );
        }

        //console.log( this );
    }

    getCurrentTabName() {
        return this._currentTabName;
    }

    setMediator( mediator ) {
        this._mediator = mediator;
    }

    // clear( tabName ) {
    //     let clearingTab = this._tabs[tabName];
    //     clearingTab.contentElem.innerHTML = '';
    // }

    // update() {
    //     this._currentTabName = config.currentTab.getName();
    //     this._updateTabNavigation();
    //     this._clearTabsContent();
    //     this._setActiveTab();
    //     this._updateTabContent();
    // }

    setActiveTab( tabName ) {
        let oldTab = this._tabs[this._currentTabName];

        if ( oldTab ) {
            oldTab.tabNavNode.classList.remove( 'is-active' );
            oldTab.tabNode.classList.remove( 'is-active' );
        }

        let currentTab = this._tabs[tabName];

        if ( currentTab ) {
            currentTab.tabNavNode.classList.add( 'is-active' );
            currentTab.tabNode.classList.add( 'is-active' );
        }

        this._currentTabName = tabName;
    }

    setTabsVisibility( tabList ) {

        for ( let tabName in this._tabs ) {

            if ( !this._tabs.hasOwnProperty( tabName ) ) {
                continue;
            }

            if ( tabList.indexOf( tabName ) === -1 ) {
                this._tabs[tabName].tabNavNode.classList.add( 'is-hidden' );
            } else {
                this._tabs[tabName].tabNavNode.classList.remove( 'is-hidden' );
            }
        }
    }

    clearTabsContent() {
        for ( let tab in this._tabs ) {
            if ( this._tabs.hasOwnProperty( tab ) ) {
                this._tabs[tab].contentNode.innerHTML = '';
            }
        }
    }

    updateTabContent( tabContent ) {
        let currentTab = this._tabs[this._currentTabName];

        if ( tabContent.additionalInfo ) {
            currentTab.contentNode.appendChild( tabContent.additionalInfo );
        }

        if ( tabContent.cardList ) {
            tabContent.cardList.forEach( card => {
                currentTab.contentNode.appendChild( card );
            } );
        }

        if ( tabContent.detailsCard ) {
            this._clearDetailsTab();
            currentTab.detailsNode.appendChild( tabContent.detailsCard );
            this.rootNode.classList.add( 'is-details-visible' );
            //currentTab.detailsNode.classList.add('is-active');
        } else {
            //currentTab.detailsNode.classList.remove('is-active');
            this.rootNode.classList.remove( 'is-details-visible' );
        }
    }

    _clickHandler( event ) {
        let tabName = event.target.getAttribute( 'data-mt-name' );
        this.setActiveTab( tabName );

        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.tabChanged;
        this._mediator.stateChanged( mediatorEvent );
    }

    _clearDetailsTab() {
        this._tabs[this._currentTabName].detailsNode.innerHTML = '';
    }

    _onClickBtnHideDetailsHandler() {
        this.rootNode.classList.remove( 'is-details-visible' );

        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.hideDetails;
        this._mediator.stateChanged( mediatorEvent );
    }

    _onClickLoadmoreHandler() {
        console.log( 'click' );
        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.loadmorePinsDetails;
        this._mediator.stateChanged( mediatorEvent );
    }

    setLoadmoreVisibility( isVisible ) {

        //TODO: think about code style for IF operator
        if ( isVisible ) {
            this._tabs[this._currentTabName].loadmoreNode.classList.add( 'is-visible' );
        } else {
            this._tabs[this._currentTabName].loadmoreNode.classList.remove( 'is-visible' );
        }
    }

    get getCurrentTab() {
        return this._tabs[this._currentTabName];
    }
}
<commit_msg>tabsControl //refactoring<commit_after>"use strict";

import BaseComponent from 'generic/baseComponent';
import MediatorEvents from '../enums/mediatorEvents';
import MediatorEventModel from '../models/mediatorEventModel';

export default class TabsControl extends BaseComponent {
    constructor() {
        super( 'map-tabs' );
        this.prefix = 'mt';

        this._tabs = {};
        this._detailsNode = null;
        this._btnHideDetails = null;
        this._mediator = null;
        this._currentTabName = '';

        this.init();
    }

    init() {
        let navItemNodes = this.rootNode.querySelectorAll( '[data-mt-elem=nav-list] > [data-mt-elem=nav-item]' );
        navItemNodes = Array.prototype.slice.call( navItemNodes );

        navItemNodes.forEach( ( navItemNode ) => {
            let tabName = navItemNode.getAttribute( 'data-mt-name' ),
                tabNode = this.rootNode.querySelector( ' [data-mt-elem=tab-item][data-mt-name=' + tabName + ']' );

            this._tabs[tabName] = {
                tabNavNode: navItemNode,
                tabNode: tabNode,
                contentNode: tabNode.querySelector( '[data-mt-elem=tab-container][data-mt-name=content]' ),
                loadmoreNode: tabNode.querySelector( '[data-mt-elem=tab-container][data-mt-name=loadmore]' )
            };
        } );

        this._detailsNode = this.rootNode.querySelector( '[data-mt-elem=tab-details]' );
        this._btnHideDetails = this.rootNode.querySelector( '[data-mt-elem=hide-details]' );

        this.addListeners();
    }

    addListeners() {

        this.rootNode.addEventListener(
            'click',
            this.listeners.onClickNavItemHandler = event => this._onClickNavItemHandler( event, 'nav-item' )
        );

        this.rootNode.addEventListener(
            'click',
            this.listeners.onClickBtnLoadmoreHandler = event => this._onClickBtnLoadmoreHandler( event, 'loadmore' )
        );

        this.rootNode.addEventListener(
            'click',
            this.listeners.onClickBtnHideDetailsHandler = event => this._onClickBtnHideDetailsHandler( event, 'hide-details' )
        );
    }

    _onClickNavItemHandler( event, elemName ) {
        let target = this._findElemNode( event.target, this.rootNode, elemName );

        if ( !target ) {
            return false;
        }

        let tabName = target.getAttribute( 'data-' + this.prefix + '-name' );

        if ( this._currentTabName === tabName ) {
            return false;
        }

        this.setActiveTab( tabName );

        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.tabChanged;
        this._mediator.stateChanged( mediatorEvent );
    }

    _onClickBtnLoadmoreHandler( event, elemName ) {
        let target = this._findElemNode( event.target, this.rootNode, elemName );

        if ( !target ) {
            return false;
        }

        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.loadmorePinsDetails;
        this._mediator.stateChanged( mediatorEvent );
    }

    _onClickBtnHideDetailsHandler( event, elemName ) {
        let target = this._findElemNode( event.target, this.rootNode, elemName );

        if ( !target ) {
            return false;
        }

        this.rootNode.classList.remove( 'is-details-visible' );

        let mediatorEvent = new MediatorEventModel();
        mediatorEvent.eventType = MediatorEvents.hideDetails;
        this._mediator.stateChanged( mediatorEvent );
    }

    getCurrentTabName() {
        return this._currentTabName;
    }

    setMediator( mediator ) {
        this._mediator = mediator;
    }

    setActiveTab( tabName ) {
        let oldTab = this._tabs[this._currentTabName];

        if ( oldTab ) {
            oldTab.tabNavNode.classList.remove( 'is-active' );
            oldTab.tabNode.classList.remove( 'is-active' );
        }

        let currentTab = this._tabs[tabName];

        if ( currentTab ) {
            currentTab.tabNavNode.classList.add( 'is-active' );
            currentTab.tabNode.classList.add( 'is-active' );
        }

        this._currentTabName = tabName;
    }

    setTabsVisibility( tabList ) {

        for ( let tabName in this._tabs ) {

            if ( !this._tabs.hasOwnProperty( tabName ) ) {
                continue;
            }

            if ( tabList.indexOf( tabName ) === -1 ) {
                this._tabs[tabName].tabNavNode.classList.add( 'is-hidden' );
            } else {
                this._tabs[tabName].tabNavNode.classList.remove( 'is-hidden' );
            }
        }
    }

    clearTabsContent() {
        for ( let tab in this._tabs ) {
            if ( this._tabs.hasOwnProperty( tab ) ) {
                this._tabs[tab].contentNode.innerHTML = '';
            }
        }
    }

    updateTabContent( tabContent ) {
        let currentTab = this._tabs[this._currentTabName];

        if ( tabContent.additionalInfo ) {
            currentTab.contentNode.appendChild( tabContent.additionalInfo );
        }

        if ( tabContent.cardList ) {
            tabContent.cardList.forEach( card => {
                currentTab.contentNode.appendChild( card );
            } );
        }

        if ( tabContent.detailsCard ) {
            this._clearDetailsTab();
            this._detailsNode.appendChild( tabContent.detailsCard );
            this.rootNode.classList.add( 'is-details-visible' );
        } else {
            this.rootNode.classList.remove( 'is-details-visible' );
        }
    }

    _clearDetailsTab() {
        this._detailsNode.innerHTML = '';
    }

    setLoadmoreVisibility( isVisible ) {
        let currentTab = this._tabs[this._currentTabName];

        if ( isVisible ) {
            currentTab.loadmoreNode.classList.add( 'is-visible' );
        } else {
            currentTab.loadmoreNode.classList.remove( 'is-visible' );
        }
    }

    static get getCurrentTab() {
        return this._tabs[this._currentTabName];
    }
}
<|endoftext|>


<commit_before>'use strict';

/********************************************************************************************
 *                                                                                *
 * Plese read the following tutorial before implementing tasks:                             *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates#Date_object
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date    *
 *                                                                                *
 ********************************************************************************************/

/**
 * Parses a rfc2822 string date representation into date value
 * For rfc2822 date specification refer to : http://tools.ietf.org/html/rfc2822#page-14
 *
 * @param {string} value
 * @return {date}
 *
 * @example:
 *    'December 17, 1995 03:24:00'    => Date()
 *    'Tue, 26 Jan 2016 13:48:02 GMT' => Date()
 *    'Sun, 17 May 1998 03:00:00 GMT+01' => Date()
 */
function parseDataFromRfc2822(value) {
   throw new Error('Not implemented');
}

/**
 * Parses an ISO 8601 string date representation into date value
 * For ISO 8601 date specification refer to : https://en.wikipedia.org/wiki/ISO_8601
 *
 * @param {string} value
 * @return {date}
 *
 * @example :
 *    '2016-01-19T16:07:37+00:00'    => Date()
 *    '2016-01-19T08:07:37Z' => Date()
 */
function parseDataFromIso8601(value) {
   throw new Error('Not implemented');
}

/**
 * Returns true if specified date is leap year and false otherwise
 * Please find algorithm here: https://en.wikipedia.org/wiki/Leap_year#Algorithm
 *
 * @param {date} date
 * @return {bool}
 *
 * @example :
 *    Date(1900,1,1)    => false
 *    Date(2000,1,1)    => true
 *    Date(2001,1,1)    => false
 *    Date(2012,1,1)    => true
 *    Date(2015,1,1)    => false
 */
function isLeapYear(date) {
   throw new Error('Not implemented');
}

/**
 * Returns the string represention of the timespan between two dates.
 * The format of output string is "HH:mm:ss.sss"
 *
 * @param {date} startDate
 * @param {date} endDate
 * @return {string}
 *
 * @example:
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,11,0,0)   => "01:00:00.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,30,0)       => "00:30:00.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,0,20)        => "00:00:20.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,0,0,250)     => "00:00:00.250"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,15,20,10,453)   => "05:20:10.453"
 */
function timeSpanToString(startDate, endDate) {
   throw new Error('Not implemented');
}

/**
 * Returns the angle (in radians) between the hands of an analog clock for the specified Greenwich time.
 * If you have problem with solution please read: https://en.wikipedia.org/wiki/Clock_angle_problem
 *
 * @param {date} date
 * @return {number}
 *
 * @example:
 *    Date.UTC(2016,2,5, 0, 0) => 0
 *    Date.UTC(2016,3,5, 3, 0) => Math.PI/2
 *    Date.UTC(2016,3,5,18, 0) => Math.PI
 *    Date.UTC(2016,3,5,21, 0) => Math.PI/2
 */
function angleBetweenClockHands(date) {
    throw new Error('Not implemented');
}

module.exports = {
    parseDataFromRfc2822: parseDataFromRfc2822,
    parseDataFromIso8601: parseDataFromIso8601,
    isLeapYear: isLeapYear,
    timeSpanToString: timeSpanToString,
    angleBetweenClockHands: angleBetweenClockHands
};
<commit_msg>Done the date tasks<commit_after>'use strict';

/********************************************************************************************
 *                                                                                *
 * Plese read the following tutorial before implementing tasks:                             *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates#Date_object
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date    *
 *                                                                                *
 ********************************************************************************************/

/**
 * Parses a rfc2822 string date representation into date value
 * For rfc2822 date specification refer to : http://tools.ietf.org/html/rfc2822#page-14
 *
 * @param {string} value
 * @return {date}
 *
 * @example:
 *    'December 17, 1995 03:24:00'    => Date()
 *    'Tue, 26 Jan 2016 13:48:02 GMT' => Date()
 *    'Sun, 17 May 1998 03:00:00 GMT+01' => Date()
 */
function parseDataFromRfc2822(value) {
   return Date.parse(value);
}

/**
 * Parses an ISO 8601 string date representation into date value
 * For ISO 8601 date specification refer to : https://en.wikipedia.org/wiki/ISO_8601
 *
 * @param {string} value
 * @return {date}
 *
 * @example :
 *    '2016-01-19T16:07:37+00:00'    => Date()
 *    '2016-01-19T08:07:37Z' => Date()
 */
function parseDataFromIso8601(value) {
   return Date.parse(value);
}

/**
 * Returns true if specified date is leap year and false otherwise
 * Please find algorithm here: https://en.wikipedia.org/wiki/Leap_year#Algorithm
 *
 * @param {date} date
 * @return {bool}
 *
 * @example :
 *    Date(1900,1,1)    => false
 *    Date(2000,1,1)    => true
 *    Date(2001,1,1)    => false
 *    Date(2012,1,1)    => true
 *    Date(2015,1,1)    => false
 */
function isLeapYear(date) {
  var year = date.getFullYear();
   return (year % 4 !== 0) ? false : (year % 100 !== 0) ? true : (year % 400 !== 0) ? false: true;
}

/**
 * Returns the string represention of the timespan between two dates.
 * The format of output string is "HH:mm:ss.sss"
 *
 * @param {date} startDate
 * @param {date} endDate
 * @return {string}
 *
 * @example:
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,11,0,0)   => "01:00:00.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,30,0)       => "00:30:00.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,0,20)        => "00:00:20.000"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,10,0,0,250)     => "00:00:00.250"
 *    Date(2000,1,1,10,0,0),  Date(2000,1,1,15,20,10,453)   => "05:20:10.453"
 */
function timeSpanToString(startDate, endDate) {
  var time = new Date(endDate - startDate),
    hours = '' + time.getUTCHours(),
    minutes = '' + time.getMinutes(),
    seconds = '' + time.getSeconds(),
    milliseconds = '' + time.getMilliseconds();
  return `${hours.length < 2 ? '0' + hours : hours}:${minutes.length < 2 ? '0' + minutes : minutes}:${seconds.length < 2 ? '0' + seconds : seconds}.${milliseconds.length < 3 ? milliseconds+'00' : milliseconds}`;
}

/**
 * Returns the angle (in radians) between the hands of an analog clock for the specified Greenwich time.
 * If you have problem with solution please read: https://en.wikipedia.org/wiki/Clock_angle_problem
 *
 * @param {date} date
 * @return {number}
 *
 * @example:
 *    Date.UTC(2016,2,5, 0, 0) => 0
 *    Date.UTC(2016,3,5, 3, 0) => Math.PI/2
 *    Date.UTC(2016,3,5,18, 0) => Math.PI
 *    Date.UTC(2016,3,5,21, 0) => Math.PI/2
 */
function angleBetweenClockHands(date) {
  var hours =  date.getUTCHours() > 12 ? date.getUTCHours() - 12 : date.getUTCHours(),
    minutes = hours * 60 + date.getMinutes(),
    hArr = minutes * 0.5,
    mArr = minutes * 6,
    dif = (mArr - hArr) % 360,
    res = 360 - dif < dif ? 360 - dif : dif;
    return Math.PI * res / 180;
}

module.exports = {
    parseDataFromRfc2822: parseDataFromRfc2822,
    parseDataFromIso8601: parseDataFromIso8601,
    isLeapYear: isLeapYear,
    timeSpanToString: timeSpanToString,
    angleBetweenClockHands: angleBetweenClockHands
};
<|endoftext|>


<commit_before>/**
 * Created by sam on 13.11.2016.
 */

const spawnSync = require('child_process').spawnSync;
const path = require('path');
const resolve = path.resolve;
const fs = require('fs');

function dependencyGraph(cwd) {
  const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
  const r = spawnSync(npm, ['ls', '--prod', '--json'], {
    cwd: cwd
  });
  if (!r.stdout) {
    console.error(cwd, r.error);
    return {};
  }
  return JSON.parse(r.stdout.toString());
}

function gitHead(cwd) {
  if (!fs.existsSync(cwd + '/.git')) {
    return null;
  }
  const r = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
    cwd: cwd
  });
  if (!r.stdout) {
    console.error(cwd, r.error);
    return {};
  }
  return r.stdout.toString().trim();
}

function resolveModules() {
  const reg = fs.readFileSync('../phovea_registry.js').toString();
  const regex = /^import '(.*)\/phovea_registry.js'/gm;
  const modules = [];
  let r;
  while ((r = regex.exec(reg)) !== null) {
    modules.push(r[1]);
  }
  return modules;
}

function cleanupDependency(d) {
  return d;
}

function resolveWorkspace() {
  console.log('resolve parent');
  const workspaceDeps = dependencyGraph('..').dependencies;
  const modules = new Set(resolveModules());

  let deps = null;
  const resolveModule = (m) => {
    console.log('resolve', m);
    const pkg = JSON.parse(fs.readFileSync(`../${m}/package.json`).toString());
    const head = gitHead('../' + m);
    const repo = pkg.repository.url;
    return {
      name: pkg.name,
      version: pkg.version,
      resolved: head ? `${repo.endsWith('.git') ? repo.slice(0, repo.length - 4) : repo}/commit/${head}` : pkg.version,
      dependencies: deps(pkg.dependencies)
    };
  };
  deps = (deps) => {
    const r = {};
    Object.keys(deps).forEach((d) => {
      if (d in workspaceDeps) {
        r[d] = cleanupDependency(workspaceDeps[d]);
        delete workspaceDeps[d];
      } else if (modules.has(d)) {
        modules.delete(d);
        r[d] = resolveModule(d);
      } else {
        r[d] = '-> link';
      }
    });
    return r;
  };

  // self =
  const root = path.basename(process.cwd());
  console.log(root);

  modules.delete(root);
  const base = resolveModule(root);
  base.extraDependencies = {};
  while (modules.size > 0) {
    let m = Array.from(modules.keys())[0];
    modules.delete(m);
    base.extraDependencies[m] = resolveModule(m);
  }
  return base;
}

function resolveSingle() {
  console.log('resolve self');
  const self = dependencyGraph('.');
  const pkg = require(`./package.json`);
  const head = gitHead('.');
  const deps = {};
  Object.keys(self.dependencies || {}).forEach((d) => {
    deps[d] = cleanupDependency(self.dependencies[d]);
  });
  return {
    name: self.name,
    version: pkg.version,
    resolved: head ? `${pkg.repository.url}#${head}` : pkg.version,
    dependencies: deps,
    extraDependencies: {}
  };
}

function generate() {
  console.log('building buildInfo');
  const isWorkspaceContext = fs.existsSync('../phovea_registry.js');
  if (isWorkspaceContext) {
    return resolveWorkspace();
  }
  return resolveSingle();
}

const IS_WINDOWS = process.platform === 'win32';

function tmpdir() {
  if (IS_WINDOWS) {
    return process.env.TEMP || process.env.TMP ||
      (process.env.SystemRoot || process.env.windir) + '\\temp';
  }
  return process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp';
}

function resolveScreenshot() {
  const f = resolve(__dirname, 'media/screenshot.png');
  if (!fs.existsSync(f)) {
    return null;
  }
  const buffer = Buffer.from(fs.readFileSync(f)).toString('base64');
  return `data:image/png;base64,${buffer}`;
}

function metaData(pkg) {
  pkg = pkg || require(`./package.json`);
  return {
    name: pkg.name,
    displayName: pkg.displayName,
    version: pkg.version,
    repository: pkg.repository.url,
    homepage: pkg.homepage,
    description: pkg.description,
    screenshot: resolveScreenshot()
  };
}

module.exports.metaData = metaData;
module.exports.metaDataTmpFile = function (pkg) {
  const s = metaData(pkg);
  const file = `${tmpdir()}/metaData${Math.random().toString(36).slice(-8)}.txt`;
  fs.writeFileSync(file, JSON.stringify(s, null, ' '));
  return file;
};
module.exports.generate = generate;
module.exports.tmpFile = function () {
  const s = generate();
  const file = `${tmpdir()}/buildInfo${Math.random().toString(36).slice(-8)}.txt`;
  fs.writeFileSync(file, JSON.stringify(s, null, ' '));
  return file;
};

if (require.main === module) {
  fs.writeFile('deps.json', JSON.stringify(generate(), null, ' '));
}
<commit_msg>Delete buildInfo.js<commit_after><|endoftext|>


<commit_before>
if (!window.ClientConfig) {
	console.error("ClientConfig has not been provided...");
}

// Add some offline UX
window.addEventListener("load", function() {

	var offlineDiv = document.createElement("div");
	offlineDiv.className = "connection";
	document.body.appendChild(offlineDiv);

	function updateOnlineStatus() {
		var condition = navigator.onLine ? "Online" : "Offline";
		document.querySelector(".connection").innerHTML = condition;
		if (condition === "Online") {
			offlineDiv.className = "connection online";
			setTimeout(function(){
				document.querySelector(".connection").innerHTML = "";
			}, 1000 * 10000);
		} else {
			offlineDiv.className = "connection offline";
		}
	}

	window.addEventListener("online",  updateOnlineStatus);
	window.addEventListener("offline", updateOnlineStatus);
});

angular.module("3drepo", ["ui.router", "ngMaterial", "ngAnimate", "ngSanitize", "vcRecaptcha"]);<commit_msg>ISSUE #394 - make timeout for offline removal sensible!<commit_after>
if (!window.ClientConfig) {
	console.error("ClientConfig has not been provided...");
}

// Add some offline UX
window.addEventListener("load", function() {

	var offlineDiv = document.createElement("div");
	offlineDiv.className = "connection";
	document.body.appendChild(offlineDiv);

	function updateOnlineStatus() {
		var condition = navigator.onLine ? "Online" : "Offline";
		document.querySelector(".connection").innerHTML = condition;
		if (condition === "Online") {
			offlineDiv.className = "connection online";
			setTimeout(function(){
				document.querySelector(".connection").innerHTML = "";
			}, 1000 * 20); // Show it for twenty seconds
		} else {
			offlineDiv.className = "connection offline";
		}
	}

	window.addEventListener("online",  updateOnlineStatus);
	window.addEventListener("offline", updateOnlineStatus);
});

angular.module("3drepo", ["ui.router", "ngMaterial", "ngAnimate", "ngSanitize", "vcRecaptcha"]);<|endoftext|>


<commit_before>import Ember from 'ember';
import CpPanelBodyComponent from 'ember-collapsible-panel/components/cp-panel-body';
import PreprintFormFieldMixin from '../mixins/preprint-form-field';

export default CpPanelBodyComponent.extend(PreprintFormFieldMixin, {
    taxonomies: {
        'a': {
            'b': ['c', 'd', 'e'],
            'f': ['g']
        },
        'h': {
            'i': ['j','k']
        },
        'l': {}
    },
    sortedTaxonomies: Ember.computed('taxonomies', function() {
        return [{
            name: 'a',
            children: [{
                name: 'b',
                children: ['c', 'd', 'e']
            }, {
                name: 'f',
                children: ['g']
            }],
        }, {
            name: 'h',
            children: [{
                name: 'i',
                children: ['j', 'k']
            }]
        }, {
            name: 'l'
        }];
    }),
    taxonomy: null,
    path: [],
    selected: new Ember.Object(),
    // sortedSelection: Ember.computed('selected', function() {
    //     const selected = this.get('selected');
    //     const taxonomies = Object.keys(selected);
    //     const temp = taxonomies
    //         .map(taxonomy => selected.get(taxonomy))
    //         .filter(categories => categories);
    //     const categories = temp
    //         .reduce((prev, cur) => prev.concat(Object.keys(cur)), []);
    //     const subjects = temp
    //         .map(categories => Object.keys(categories)
    //             .map(category => categories.get(category))
    //             .filter(subjects => subjects)
    //             .reduce((prev, cur) => prev.concat(Object.keys(cur)), []))
    //         .reduce((prev, cur) => prev.concat(cur));
    //     return {
    //         taxonomies: taxonomies,
    //         categories: categories,
    //         subjects: subjects
    //     };
    // }),
    sortedSelection: Ember.computed('selected', function() {
        const sorted = [];
        const selected = this.get('selected');
        const flatten = ([obj, name = []]) => {
            const keys = Object.keys(obj);
            if (keys.length === 0) {
                return name.length !== 0 && sorted.pushObject(name);
            } else {
                return keys.sort().map(key => [obj.get(key), [...name, key]]).forEach(flatten);
            }
        };
        flatten([selected]);
        return sorted;
    }),
    valid: Ember.computed.oneWay('taxonomy'),
    actions: {
        delete(key) {
            this.set(key, null);
            // Handle keys with spaces
            eval(`delete this['${key.replace(/\./g, "']['")}']`);
        },
        deselect([...args]) {
            this.send('delete', `selected.${args.filter(arg => Ember.typeOf(arg) === 'string').join('.')}`);
            this.notifyPropertyChange('selected');
            this.rerender();
        },
        select(...args) {
            const process = (prev, cur, i) => {
                if (!this.get(`selected.${prev}`)) {
                    // Create necessary parent objects and newly selected object
                    this.set(`selected.${prev}`, new Ember.Object());
                } else if (i === args.length && this.get('path').every((e, i) => e === args[i])
                    && Object.keys(this.get(`selected.${prev}`)).length === 0) {
                    // Deselecting a subject: if subject is last item in args,
                    // its children are showing, and no children are selected
                    this.send('delete', `selected.${prev}`);
                    args.popObject();
                }
                return `${prev}.${cur}`;
            };
            // Process past length of array
            process(args.map(arg => arg.name || arg).reduce(process), '', args.length);
            this.set('path', args);
            this.notifyPropertyChange('selected');
            this.rerender();
        }
    }
});
<commit_msg>Add simple subject validation<commit_after>import Ember from 'ember';
import CpPanelBodyComponent from 'ember-collapsible-panel/components/cp-panel-body';
import PreprintFormFieldMixin from '../mixins/preprint-form-field';

export default CpPanelBodyComponent.extend(PreprintFormFieldMixin, {
    taxonomies: {
        'a': {
            'b': ['c', 'd', 'e'],
            'f': ['g']
        },
        'h': {
            'i': ['j','k']
        },
        'l': {}
    },
    sortedTaxonomies: Ember.computed('taxonomies', function() {
        return [{
            name: 'a',
            children: [{
                name: 'b',
                children: ['c', 'd', 'e']
            }, {
                name: 'f',
                children: ['g']
            }],
        }, {
            name: 'h',
            children: [{
                name: 'i',
                children: ['j', 'k']
            }]
        }, {
            name: 'l'
        }];
    }),
    path: [],
    selected: new Ember.Object(),
    // sortedSelection: Ember.computed('selected', function() {
    //     const selected = this.get('selected');
    //     const taxonomies = Object.keys(selected);
    //     const temp = taxonomies
    //         .map(taxonomy => selected.get(taxonomy))
    //         .filter(categories => categories);
    //     const categories = temp
    //         .reduce((prev, cur) => prev.concat(Object.keys(cur)), []);
    //     const subjects = temp
    //         .map(categories => Object.keys(categories)
    //             .map(category => categories.get(category))
    //             .filter(subjects => subjects)
    //             .reduce((prev, cur) => prev.concat(Object.keys(cur)), []))
    //         .reduce((prev, cur) => prev.concat(cur));
    //     return {
    //         taxonomies: taxonomies,
    //         categories: categories,
    //         subjects: subjects
    //     };
    // }),
    sortedSelection: Ember.computed('selected', function() {
        const sorted = [];
        const selected = this.get('selected');
        const flatten = ([obj, name = []]) => {
            const keys = Object.keys(obj);
            if (keys.length === 0) {
                return name.length !== 0 && sorted.pushObject(name);
            } else {
                return keys.sort().map(key => [obj.get(key), [...name, key]]).forEach(flatten);
            }
        };
        flatten([selected]);
        return sorted;
    }),
    valid: Ember.computed('selected', function() {
        return Object.keys((this.get('selected'))).length !== 0;
    }),
    actions: {
        delete(key) {
            this.set(key, null);
            // Handle keys with spaces
            eval(`delete this['${key.replace(/\./g, "']['")}']`);
        },
        deselect([...args]) {
            this.send('delete', `selected.${args.filter(arg => Ember.typeOf(arg) === 'string').join('.')}`);
            this.notifyPropertyChange('selected');
            this.rerender();
        },
        select(...args) {
            const process = (prev, cur, i) => {
                if (!this.get(`selected.${prev}`)) {
                    // Create necessary parent objects and newly selected object
                    this.set(`selected.${prev}`, new Ember.Object());
                } else if (i === args.length && this.get('path').every((e, i) => e === args[i])
                    && Object.keys(this.get(`selected.${prev}`)).length === 0) {
                    // Deselecting a subject: if subject is last item in args,
                    // its children are showing, and no children are selected
                    this.send('delete', `selected.${prev}`);
                    args.popObject();
                }
                return `${prev}.${cur}`;
            };
            // Process past length of array
            process(args.map(arg => arg.name || arg).reduce(process), '', args.length);
            this.set('path', args);
            this.notifyPropertyChange('selected');
            this.rerender();
        }
    }
});
<|endoftext|>