Say I have a "type" like this:
{
a: {
b: {
c: {
d: string
e: boolean
}
},
x: string
y: number
z: string
}
}
At each object node, I want to get notified if all of the children are "resolved" to a value. So for example:
const a = new TreeObject()
a.watch((a) => console.log('a resolved', a))
const b = a.createObject('b')
b.watch((b) => console.log('b resolved', b))
const c = b.createObject('c')
c.watch((c) => console.log('c resolved', c))
const d = c.createLiteral('d')
d.watch((d) => console.log('d resolved', d))
const e = c.createLiteral('e')
e.watch((e) => console.log('e resolved', e))
const x = a.createLiteral('x')
x.watch((x) => console.log('x resolved', x))
const y = a.createLiteral('y')
y.watch((y) => console.log('y resolved', y))
d.set('foo')
// logs:
// d resolved
e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved
y.set('hello')
// logs:
// y resolved
x.set('world')
// logs:
// x resolved
// a resolved
That is the base case. The more complex case, which is what I've been trying to solve for, is matching against a subset of properties, like this:
// receive 'b' only if b.c.d is resolved.
// '3' for 3 args
a.watch3('b', {
c: {
d: true
}
}, () => {
console.log('b with b.c.d resolved')
})
You can have multiple "watchers" per property node, like this:
a.watch3('b', { c: { d: true } }, () => {
console.log('get b with b.c.d resolved')
})
a.watch3('b', { c: { e: true } }, () => {
console.log('get b with b.c.e resolved')
})
a.watch2('x', () => {
console.log('get x when resolved')
})
// now if were were to start from scratch setting properties fresh:
x.set('foo')
// logs:
// get x when resolved
e.set('bar')
// logs:
// get b with b.c.e resolved
How can you neatly set this up? I have been trying for a long time to wrap my head around it but not getting far (as seen in this TS playground.
type Matcher = {
[key: string]: true | Matcher
}
type Callback = () => void
class TreeObject {
properties: Record<string, unknown>
callbacks: Record<string, Array<{ matcher?: Matcher, callback: Callback }>>
parent?: TreeObject
resolved: Array<Callback>
constructor(parent?: TreeObject) {
this.properties = {}
this.callbacks = {}
this.parent = parent
this.resolved = []
}
createObject(name: string) {
const tree = new TreeObject(this)
this.properties[name] = tree
return tree
}
createLiteral(name: string) {
const tree = new TreeLiteral(this, () => {
// somehow start keeping track of decrementing what we have matched so far
// and when it is fully decremented, trigger the callback up the chain.
})
this.properties[name] = tree
return tree
}
watch3(name: string, matcher: Matcher, callback: Callback) {
const list = this.callbacks[name] ??= []
list.push({ matcher, callback })
}
watch2(name: string, callback: Callback) {
const list = this.callbacks[name] ??= []
list.push({ callback })
}
watch(callback: Callback) {
this.resolved.push(callback)
}
}
class TreeLiteral {
value: any
parent: TreeObject
callback: () => void
resolved: Array<Callback>
constructor(parent: TreeObject, callback: () => void) {
this.value = undefined
this.parent = parent
this.callback = callback
this.resolved = []
}
set(value: any) {
this.value = value
this.resolved.forEach(resolve => resolve())
this.callback()
}
watch(callback: Callback) {
this.resolved.push(callback)
}
}
const a = new TreeObject()
a.watch(() => console.log('a resolved'))
const b = a.createObject('b')
b.watch(() => console.log('b resolved'))
const c = b.createObject('c')
c.watch(() => console.log('c resolved'))
const d = c.createLiteral('d')
d.watch(() => console.log('d resolved'))
const e = c.createLiteral('e')
e.watch(() => console.log('e resolved'))
const x = a.createLiteral('x')
x.watch(() => console.log('x resolved'))
const y = a.createLiteral('y')
y.watch(() => console.log('y resolved'))
d.set('foo')
// logs:
// d resolved
e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved
y.set('hello')
// logs:
// y resolved
x.set('world')
// logs:
// x resolved
// a resolved
How can you define the watch3
and related methods to accept their "matchers" and callback, and properly call the callback when the matchers' properties are all fulfilled?
It gets tricky because you can work in two directions:
- The value could have already been resolved in the past, before you added your watchers/listeners. It should still be notified right away in that case.
- The value can be resolved in the future, after you added your watchers. It should be notified only once fulfilled.
Note, the "matcher" syntax is sort of like a GraphQL query, where you simply build an object tree with the leaves set to true
on what you want.