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