class Graph {
constructor() {
class PrivateLazyNode extends LazyNode {}
PrivateLazyNode.prototype.graph = this
this.depsStack = []
this.nodeStack = []
this.currentDeps = undefined
this.currentNode = undefined
this.traceNames = undefined
this.nextNodeId = 0
this.lazyNodeClass = PrivateLazyNode
this.tick = Promise.resolve()
}
trace(name) {
if (!this.traceNames) {
this.traceNames = new Set()
}
this.traceNames.add(name)
}
_newNode(name) {
this.nextNodeId++
return new this.lazyNodeClass(name, this.traceNames ? this.traceNames.has(name) : false)
}
newNode(name) {
const node = this._newNode(name)
node.used = () => this.used(node)
return node
}
used(node) {
if (this.currentDeps !== undefined) {
if (this.currentNode === node) {
throw new Error(`node can't depend on itself!!! [${node.name}]`)
}
this.currentDeps.add(node)
}
}
free(node) {
if (this.currentDeps !== undefined) {
this.currentDeps.delete(node)
}
for (const set of this.depsStack) {
set.delete(node)
}
}
runDiscoveringDeps(node, fn, cb) {
if (this.currentDeps !== undefined) {
this.depsStack.push(this.currentDeps)
this.nodeStack.push(this.currentNode)
}
const deps = new Set()
this.currentNode = node
this.currentDeps = deps
let result
let error
try {
result = fn()
} catch (e) {
error = e
}
this.currentDeps = this.depsStack.pop()
this.currentNode = this.nodeStack.pop()
cb(deps.size === 0 ? undefined : Array.from(deps), result, error)
if (error) {
throw error
}
return result
}
*yieldPromises(gen, name) {
let o
// let invalidations = 0
try {
o = this.runAndPromiseInvalidation(() => gen.next(), name)
// console.log("yielding", o.value.value)
yield Promise.resolve(o.value.value)
while (!o.value.done) {
// console.log("yielding again")
yield new Promise(resolve => {
o.invalidation.then(() => {
// console.log("invalidated", o.value.value)
// if (invalidations++ > 100) {
// debugger
// gen.return()
// }
this.tick.then(() => {
o = this.runAndPromiseInvalidation(() => gen.next(), name)
// console.log("resolving", o.value.value)
resolve(o.value.value)
})
})
})
}
} finally {
// console.log("cancelled", o.value.value)
gen.return()
if (o && o.cancel !== undefined) {
o.cancel()
}
}
}
runAndPromiseInvalidation(fn, name) {
const node = this._newNode("promise::" + name + "::" + this.nextNodeId)
let deps
const value = this.runDiscoveringDeps(
node,
fn,
(d, data) => (
deps = d,
node.mark(d, data),
this.used(node)),
)
let pseudoNode
let reject
const invalidation = new Promise((resolve, reject_) => {
pseudoNode = {
notify: (notifier) => {
resolve()
}
}
reject = reject_
node.awaitNotify(pseudoNode)
})
return {
deps,
invalidation,
cancel() {
node.abortNotify(pseudoNode)
node.mark(undefined, "abort")
// Don't actually reject right now since it's not part of our spec yet...
// reject()
},
value,
}
}
newLazyProxy(cache, upstream, namePrefix) {
let nodes = new Map()
return {
revoke() {
for (const node of nodes.values()) {
node.mark(undefined, "revoked")
}
nodes = null
},
proxy: new Proxy(upstream, {
deleteProperty: (target, prop) => {
if (nodes === null) {
throw new TypeError(`Cannot delete "${prop}", already revoked "${namePrefix}"`)
}
let node = nodes.get(prop)
if (node === undefined) {
return true
}
delete cache[prop]
nodes.delete(prop)
node.mark(undefined, "prop deleted")
return true
},
get: (target, prop, receiver) => {
if (nodes === null) {
throw new TypeError(`Cannot get "${prop}", already revoked "${namePrefix}"`)
}
let node = nodes.get(prop)
let result = undefined
if (node === undefined || node.stale) {
// If node is stale or we've never calculated this value before
this.runDiscoveringDeps(
node,
() => {
result = Reflect.get(target, prop, receiver)
if (result === undefined) {
// Otherwise we don't have a value, so stop caching it.
return
}
// If we have a value, cache it.
cache[prop] = result
return result
},
(deps, data) => {
// Now that we know deps...
if (result === undefined) {
// If we don't have a value now, but we used to, clear the previous mark and return
if (node !== undefined) {
node.mark(undefined, "deleted")
nodes.delete(prop)
delete cache[prop]
node = undefined
}
// Re-mark all of the discovered deps as used so that we know to retry this if any of them change
if (deps) {
for (const dep of deps) {
this.used(dep)
}
}
return
}
// Otherwise we have a result now so we'll want to either create a new node if needed
if (node === undefined) {
node = this._newNode(namePrefix ? namePrefix + prop : prop)
nodes.set(prop, node)
}
if (node && node.debug) {
node.log("lazy deps", deps)
}
// and populate that node with deps
node.mark(deps, data)
},
)
} else {
result = cache[prop]
}
// if we've made it this far and we have a node, mark it as used
if (node) {
this.used(node)
}
return result
},
})
}
}
defineProperties(o, properties, namePrefix) {
for (const k in properties) {
const property = properties[k]
if ("value" in property) {
let node
let value = property.value
Object.defineProperty(o, k, {
enumerable: property.enumerable,
configurable: property.configurable,
get: () => {
if (node === undefined) {
node = this._newNode(namePrefix ? namePrefix + k : k).mark(undefined, value)
}
this.used(node)
return value
},
set: property.writable == false ? undefined : next => {
if (value !== next) {
if (node !== undefined) {
node.mark(undefined, next)
}
value = next
}
if (node === undefined) {
node = this._newNode(namePrefix ? namePrefix + k : k).mark(undefined, value)
}
this.used(node)
return true
},
})
} else {
let node
let value
const getter = property.get
Object.defineProperty(o, k, {
enumerable: property.enumerable,
configurable: property.configurable,
get: () => {
if (node === undefined) {
node = this._newNode(namePrefix ? namePrefix + k : k)
} else if (!node.stale) {
this.used(node)
return value
}
return this.runDiscoveringDeps(
node,
() => value = getter.call(o),
(deps, data) => (node.mark(deps, data), this.used(node)),
)
},
})
}
}
return o
}
}