<commit_before>import most from 'most';

import { Set } from './util';


export default function Events(componentDestruction) {
  this.componentDestruction = componentDestruction;
  componentDestruction.observe(this._destroy);
};

Events.prototype.listen = function(selector, evt) {
  this.eventSubs = this.eventSubs || [];

  const stream = most.create(add => sub.streamAdd = add)
    .until(this.componentDestruction);

  const sub = { selector, evt, isCustom: evt._isCustom };
  this.eventSubs.push(sub);
  if (this.el) subscribe(sub, this.el);

  return stream;
};

Events.prototype.emit = function(event) {
  if (!this.eventSubs) return;

  let parentEl = this.el.parentElement;

  event.targetComponent = this.el;

  while (parentEl) {
    if (parentEl.__comp__)
      parentEl.__comp__.events._handleEmit(event);

    parentEl = parentEl.parentElement;
  }
};

Events.prototype._activate = function(el) {
  if (!this.eventSubs) return;

  this.el = el;
  this.eventSubs.forEach(sub => subscribe(sub, el));
};

Events.prototype._destroy = function() {
  if (!this.eventSubs) return;

  this.eventSubs.forEach(sub => {
    if (sub.isCustom) return;
    const { evt, listener: { fn, useCapture }} = sub;
    this.el.removeEventListener(evt, fn, useCapture);
  });

  this.eventSubs = null;
};

Events.prototype._handleEmit = function(event) {
  const subs = this.eventSubs;
  if (!subs) return;

  for (let i = 0; i < subs.length; i++) {
    const sub = subs[i];
    if (sub.isCustom && matches(event.targetComponent, sub.selector))
      sub.streamAdd(event.payload);
  }
};

function subscribe(sub, el) {
  if (sub.isCustom) return;

  const listener = evt => {
    if (targetMatches(evt.target, sub.selector, el))
      sub.streamAdd(evt);
  }
  const useCapture = sub.evt in nonBubblingEvents;
  el.addEventListener(sub.evt, listener, useCapture);
  sub.listener = { fn: listener, useCapture };
}


const proto = Element.prototype;
const nativeMatches = proto.matches
  || proto.matchesSelector
  || proto.webkitMatchesSelector
  || proto.mozMatchesSelector
  || proto.msMatchesSelector
  || proto.oMatchesSelector;
function matches(el, selector) {
  return nativeMatches.call(el, selector);
}

const nonBubblingEvents = Set(
  'load',
  'unload',
  'focus',
  'blur',
  'mouseenter',
  'mouseleave',
  'submit',
  'change',
  'reset',
  'timeupdate',
  'playing',
  'waiting',
  'seeking',
  'seeked',<commit_msg>Better event stream memory management<commit_after>import most from 'most';

import { Set } from './util';


export default function Events(componentDestruction) {
  this.componentDestruction = componentDestruction;
  componentDestruction.observe(() => this._destroy());
};

Events.prototype.listen = function(selector, evt) {
  this.eventSubs = this.eventSubs || [];

  const stream = most.create(add => {
    const sub = {
      selector,
      evt,
      isCustom: evt._isCustom,
      streamAdd: add
    };

    this.eventSubs.push(sub);
    subscribe(sub, this.el);

    return () => {
      // Already disposed
      if (!this.eventSubs) return;
      this.eventSubs.splice(this.eventSubs.indexOf(sub), 1);
      unsubscribe(sub, this.el);
    }
  })
  .until(this.componentDestruction);

  return stream;
};

Events.prototype.emit = function(event) {
  if (!this.eventSubs) return;

  let parentEl = this.el.parentElement;

  event.targetComponent = this.el;

  while (parentEl) {
    if (parentEl.__comp__)
      parentEl.__comp__.events._handleEmit(event);

    parentEl = parentEl.parentElement;
  }
};

Events.prototype._activate = function(el) {
  if (!this.eventSubs) return;

  this.el = el;
  this.eventSubs.forEach(sub => subscribe(sub, el));
};

Events.prototype._destroy = function() {
  if (!this.eventSubs) return;

  this.eventSubs.forEach(sub => unsubscribe(sub, this.el));
  this.eventSubs = null;
};

Events.prototype._handleEmit = function(event) {
  const subs = this.eventSubs;
  if (!subs) return;

  for (let i = 0; i < subs.length; i++) {
    const sub = subs[i];
    if (sub.isCustom && matches(event.targetComponent, sub.selector))
      sub.streamAdd(event.payload);
  }
};

function subscribe(sub, el) {
  if (!el || sub.isCustom) return;

  const listener = evt => {
    if (targetMatches(evt.target, sub.selector, el))
      sub.streamAdd(evt);
  }
  const useCapture = sub.evt in nonBubblingEvents;
  el.addEventListener(sub.evt, listener, useCapture);
  sub.listener = { fn: listener, useCapture };
}

function unsubscribe(sub, el) {
  if (!el || sub.isCustom) return;

  const { evt, listener: { fn, useCapture }} = sub;
  el.removeEventListener(evt, fn, useCapture);
}


const proto = Element.prototype;
const nativeMatches = proto.matches
  || proto.matchesSelector
  || proto.webkitMatchesSelector
  || proto.mozMatchesSelector
  || proto.msMatchesSelector
  || proto.oMatchesSelector;
function matches(el, selector) {
  return nativeMatches.call(el, selector);
}

const nonBubblingEvents = Set(
  'load',
  'unload',
  'focus',
  'blur',
  'mouseenter',
  'mouseleave',
  'submit',
  'change',
  'reset',
  'timeupdate',
  'playing',
  'waiting',
  'seeking',
  'seeked',
{'fraction_non_alphanumeric': 0.1446651090342679, 'fraction_numerical': 0.0005841121495327102, 'mean_word_length': 3.8009345794392524, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 13, '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': '3773166', 'n_tokens_mistral': 1731, 'n_tokens_neox': 1711, 'n_words': 357}