git » hoplite.git » master » tree

[master] / reactives.mjs

import { StupidTupleSet, partialQuery } from './datalog.mjs';

const depGraph = new Map(),
      suspended = new Set(),
      captured = new Set();
let inTransaction = false,
    capturing = false;

function addEdge(g, from, to) {
  let s = g.has(from) ? g.get(from) : new Set();
  s.add(to);
  g.set(from, s);
}

function removeEdge(g, from, to) {
  if (g.has(from)) {
    let s = g.get(from);
    s.delete(to);
    if (s.size === 0) {
      g.delete(from);
    } 
  }
}

function propagate(g, from, walked = new Set()) {
  if (!g.has(from)) return;
  g.get(from).forEach(to => {
    if (!walked.has(to)) {
      walked.add(to);
      if (to.update()) {
        propagate(g, to, walked);
      }
    }
  });
}

function captureCreated(thunk) {
  if (capturing) {
    thunk();
  } else {
    try {
      capturing = true;
      thunk();
    } finally {
      capturing = false;
      let capturedCopy = new Set([...captured]);
      captured.clear();
      return capturedCopy;
    }
  }
}

function transaction(thunk) {
  if (inTransaction) {
    thunk();
  } else {
    try {
      inTransaction = true;
      thunk();
    } finally {
      inTransaction = false;
      let walked = new Set();
      suspended.forEach(s => propagate(depGraph, s, walked));
      suspended.forEach(s => s.flush());
      suspended.clear();
    }
  }
}

class Input {
  constructor(value) {
    this.value = this.previousValue = value;
  }
  set(value) {
    if (this.value !== value) {
      this.previousValue = this.value;
      this.value = value;
      if (inTransaction) {
        suspended.add(this);
      } else {
        propagate(depGraph, this);
      }
    }
    return value;
  }
  flush() {
    // NOOP
  }
}

class Formula {
  constructor(f, sources) {
    this.f = f;
    this.sources = sources;
    sources
      .filter(Formula.isReactiveSource)
      .forEach(source => addEdge(depGraph, source, this));
    this.value = this.previousValue = undefined;
    this.paused = false;
    if (capturing) {
      captured.add(this);
    }
    this.update();
  }
  pause() {
    this.paused = true;
  }
  resume() {
    this.paused = false;
  }
  static isReactiveSource(x) {
    return x instanceof Input || x instanceof Formula;
  }
  detach() {
    this.sources.forEach(source => removeEdge(depGraph, source, this));
  }
  sourceValues() {
    return this.sources.map(x => Formula.isReactiveSource(x) ? x.value : x);
  }
  update() {
    if (this.paused) return false;
    let value = this.f.apply(null, this.sourceValues());
    if (this.value !== value) {
      this.previousValue = this.value;
      this.value = value;
      return true;
    }
    return false;
  }
}

class Database {
  constructor() {
    this.set = new StupidTupleSet();
    this.added = new StupidTupleSet();
    this.removed = new StupidTupleSet();
    this.maxEid = 0;
  }
  nextEid() {
    return this.maxEid + 1;
  }
  deleteEntity(eid) {
    this.remove([...this].filter(([e]) => e === eid));
  }
  add(tuples) {
    let changed = false;
    for (let tuple of tuples) {
      if (!this.set.has(tuple)) {
        changed = true;
        this.added.add(tuple);
        this.removed.remove(tuple);
        this.set.add(tuple);
        if (tuple.length > 0 && tuple[0] > this.maxEid) {
          this.maxEid = tuple[0];
        }
      }
    }
    if (changed) {
      if (inTransaction) {
        suspended.add(this);
      } else {
        propagate(depGraph, this);
        this.added.clear();
      }
    }
    return this;
  }
  remove(tuples) {
    let changed = false;
    for (let tuple of tuples) {
      if (this.set.has(tuple)) {
        changed = true;
        this.removed.add(tuple);
        this.added.remove(tuple);
        this.set.remove(tuple);
      }
    }
    if (changed) {
      if (inTransaction) {
        suspended.add(this);
      } else {
        propagate(depGraph, this);
        this.removed.clear();
      }
    }
    return this;
  }
  flush() {
    this.added.clear();
    this.removed.clear();
  }
  [Symbol.iterator]() {
    return this.set[Symbol.iterator]();
  }
}

class View {
  constructor(q, sources) {
    // q is not reactive (for now)
    this.query = q instanceof Function ? q : partialQuery(q);
    this.sources = sources;
    this.set = new StupidTupleSet();
    this.added = new StupidTupleSet();
    this.removed = new StupidTupleSet();
    this.watches = new Map();
    sources
      .filter(View.isReactiveSource)
      .forEach(source => addEdge(depGraph, source, this));
    this.paused = false;
    if (capturing) {
      captured.add(this);
    }
    this.update();
  }
  pause() {
    this.paused = true;
  }
  resume() {
    this.paused = false;
  }
  detach() {
    this.sources
      .filter(View.isReactiveSource)
      .forEach(source => removeEdge(depGraph, source, this))
  }
  static isReactiveSource(x) {
    return x instanceof Input
      || x instanceof Formula
      || x instanceof Database
      || x instanceof View;
  }
  sourceValues() {
    return this.sources.map(x => {
      if (x instanceof Input || x instanceof Formula) {
        return x.value;
      } else if (x instanceof Database || x instanceof View) {
        return x.set;
      } else {
        return x;
      }
    });
  }
  update() {
    if (this.paused) return false;
    let newSet = this.query(...this.sourceValues()),
        added = newSet.complement(this.set),
        removed = this.set.complement(newSet);
    for (let tuple of added) {
      this.added.add(tuple);
      this.removed.remove(tuple);
      this.set.add(tuple);
    }
    for (let tuple of removed) {
      this.added.remove(tuple);
      this.removed.add(tuple);
      this.set.remove(tuple);
    }
    if (added.size() || removed.size()) {
      if (inTransaction) {
        suspended.add(this);
      } else {
        propagate(depGraph, this);
        this.flush();
      }
    }
  }
  flush() {
    this.added.clear();
    this.removed.clear();
  }
}

class Watch {
  constructor(callback, source) {
    this.callback = callback;
    this.source = source;
    addEdge(depGraph, source, this)
    if (this.source instanceof Input
        || this.source instanceof Formula) {
      this.callback.call(null, this.source.previousValue, this.source.value)
    } else {
      this.callback.call(null, this.source.set, new StupidTupleSet());
    }
  }
  detach() {
    removeEdge(depGraph, this.source, this);
  }
  update() {
    if (this.source instanceof Input
        || this.source instanceof Formula) {
      this.callback.call(null, this.source.previousValue, this.source.value)
    } else {
      this.callback.call(null, this.source.added, this.source.removed)
    }
  }
  flush() {
    // NOOP
  }
}

function input(initialValue) {
  return new Input(initialValue);
}

function formula(f) {
  return (...sources) => new Formula(f, sources);
}

function database(tuples) {
  return new Database().add(tuples);
}

function view(q) {
  return (...sources) => new View(q, sources);
}

function watch(f) {
  return (source) => new Watch(f, source);
}

function cond(...pairs) {
  return formula((...pairs) => {
    for (let i = 0; i < pairs.length-1; i+=2) {
      if (pairs[i]) return pairs[i+1];
    }
    return null;
  })(...pairs);
}

function not(x) {
  return formula(v => !v)(x);
}

function and(...xs) {
  return formula((...xs) => {
    for (x of xs) {
      if (x.value) {
        return x.value;
      }
    }
    return false;
  })(...xs)
}

export { Input, Formula, transaction, captureCreated, input, formula, database, view, watch, cond, not };