Imagine having a "type" structured like this:
{
a: {
b: {
c: {
d: string
e: boolean
}
},
x: string
y: number
z: string
}
}
What if you wanted to be notified at each object node when all the children are resolved to a value? 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
This is the base case. The more complex scenario, which I've been trying to tackle, involves 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 as well:
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')
})
// If we 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 this be neatly set up? I have been struggling to figure it out but haven't made much progress. You can check my attempts 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, () => {
// Logic to track and trigger the callback once fully matched
})
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 the methods like watch3
be defined to handle matchers and call the callback appropriately when the specified properties are fulfilled?
The challenge lies in handling values that may have already been resolved before adding watchers, as well as future resolutions after adding them. The "matcher" syntax resembles GraphQL queries, where you define an object tree with leaves set to true
on desired properties.