<commit_before>import ko from 'knockout';

export default class UndoManager {
  /**
   * Determins how many undo/redo steps will be stored in memory.
   * @type {Number}
   */
  MAX_UNDO_STEPS;

  /**
   * Stack for past state snapshots
   * @type {Array}
   */
  past = [];

  /**
   * Stack for future state snapshots
   * @type {Array}
   */
  future = [];

  /**
   * [throttle description]
   * @type {Number}
   */
  throttle = 300;

  /**
   * A collection for all changes done within the {@see throttle} timeout.
   * This acts as a full rollback path.
   * @type {Array}
   */
  undoCollection = [];

  _batchTimeout = null;
  _ignoreChanges = false;

  constructor(vm, {steps = 30, throttle = 300} = {}) {
    this.MAX_UNDO_STEPS = steps;
    this.throttle       = throttle;
    this.listen(vm);
  }

  listen(vm) {
    for (let [key, item] of Object.entries(vm)) {
      if (ko.isWritableObservable(item) && !ko.isComputed(item) && ko.isSubscribable(item)) {
        const observable = item;
        let previousValue = observable.peek();

        previousValue = Array.isArray(previousValue) ? [...previousValue] : previousValue;

        observable.subscribe((nextValue) => {
          nextValue = Array.isArray(nextValue) ? [...nextValue] : nextValue;
          this.change({observable, nextValue, previousValue});
          previousValue = nextValue;
        });

        if (Array.isArray(previousValue)) {
          previousValue.forEach((item) => this.listen(item));
        }
      }
    }
  }

  change({observable, nextValue, previousValue}) {
    if (this._ignoreChanges) return;
    if (this._batchTimeout) clearTimeout(this._batchTimeout);

    const atomicChange = {observable, nextValue, previousValue};
    this.undoCollection.push(atomicChange);

    const afterCollecting = () => {
      this.past.push(this.undoCollection);
      this.past = this.past.slice(-this.MAX_UNDO_STEPS);
      this.future = [];
      this.undoCollection = [];
      this._batchTimeout = null;
    }

    if (this.throttle) this._batchTimeout = setTimeout(afterCollecting, this.throttle);
    else afterCollecting();
  }

  destroy() {
    past = [];
    future = [];
  }

  undo() {
    if (!this.past.length) return;
    if (this._batchTimeout) {
      clearTimeout(this._batchTimeout);
      this._batchTimeout = null;
    }
    const present = this.past.pop();
    this.future.push(present);

    this._ignoreChanges = true;
    present.reverse().forEach(({observable, previousValue}) => {
      if (Array.isArray(previousValue)) {
        const targetArray = [...observable.peek()];
        if (previousValue.length > targetArray.length) {
          previousValue.forEach((item) => {
            if (targetArray.includes(item)) return;
            observable.push(item);
          });
        }<commit_msg>fix(lib): Fixing behavior when changesets should be pushed to the stack.<commit_after>import ko from 'knockout';

export default class UndoManager {
  /**
   * Determins how many undo/redo steps will be stored in memory.
   * @type {Number}
   */
  MAX_UNDO_STEPS;

  /**
   * Stack for past state snapshots
   * @type {Array}
   */
  past = [];

  /**
   * Stack for future state snapshots
   * @type {Array}
   */
  future = [];


  /**
   * [throttle description]
   * @type {Number}
   */
  throttle = 300;

  /**
   * A collection for all changes done within the {@see throttle} timeout.
   * This acts as a full rollback path.
   * @type {Array}
   */
  undoCollection = [];

  _subscriptions = [];
  _batchTimeout = null;
  _ignoreChanges = false;

  constructor(vm, {steps = 30, throttle = 300} = {}) {
    console.log('Setting throttle', throttle);
    this.MAX_UNDO_STEPS = steps;
    this.throttle       = throttle;
    this.listen(vm);
  }

  listen(vm) {
    for (let [key, item] of Object.entries(vm)) {
      if (ko.isWritableObservable(item) && !ko.isComputed(item) && ko.isSubscribable(item)) {
        const observable = item;
        let previousValue = observable.peek();

        previousValue = Array.isArray(previousValue) ? [...previousValue] : previousValue;

        const subscription = observable.subscribe((nextValue) => {
          nextValue = Array.isArray(nextValue) ? [...nextValue] : nextValue;
          this.change({observable, nextValue, previousValue});
          previousValue = nextValue;
        });
        this._subscriptions.push(subscription);

        if (Array.isArray(previousValue)) {
          previousValue.forEach((item) => this.listen(item));
        }
      }
    }
  }

  change({observable, nextValue, previousValue}) {
    if (this._ignoreChanges) return;
    if (this._batchTimeout) clearTimeout(this._batchTimeout);
    else this.past.push(this.undoCollection);

    const atomicChange = {observable, nextValue, previousValue};
    this.undoCollection.push(atomicChange);

    const afterCollecting = () => {
      this.past = this.past.slice(-this.MAX_UNDO_STEPS);
      this.future = [];
      this.undoCollection = [];
      this._batchTimeout = null;
    }

    if (this.throttle) this._batchTimeout = setTimeout(afterCollecting, this.throttle);
    else afterCollecting();
  }

  destroy() {
    this.past = [];
    this.future = [];
    this._subscriptions.forEach((subscription) => subscription.dispose());
    this._subscriptions = [];
  }

  undo() {
    if (!this.past.length) return;
    if (this._batchTimeout) {
      clearTimeout(this._batchTimeout);
      this._batchTimeout = null;
    }
    const present = this.past.pop();
    this.future.push(present);

    this._ignoreChanges = true;
    present.reverse().forEach(({observable, previousValue}) => {
      if (Array.isArray(previousValue)) {
        const targetArray = [...observable.peek()];
        if (previousValue.length > targetArray.length) {
          previousValue.forEach((item) => {
            if (targetArray.includes(item)) return;
            observable.push(item);
          });
        }
{'fraction_non_alphanumeric': 0.13812804570660392, 'fraction_numerical': 0.0026886237607124854, 'mean_word_length': 2.690018598884067, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4677687', 'n_tokens_mistral': 1862, 'n_tokens_neox': 1781, 'n_words': 375}