kopia lustrzana https://github.com/Tldraw/Tldraw
172 wiersze
4.6 KiB
TypeScript
172 wiersze
4.6 KiB
TypeScript
import { attach, detach, singleton } from './helpers'
|
|
import type { Child, Signal } from './types'
|
|
|
|
class CaptureStackFrame {
|
|
offset = 0
|
|
numNewParents = 0
|
|
|
|
maybeRemoved?: Signal<any>[]
|
|
|
|
constructor(
|
|
public readonly below: CaptureStackFrame | null,
|
|
public readonly child: Child
|
|
) {}
|
|
}
|
|
|
|
const inst = singleton('capture', () => ({ stack: null as null | CaptureStackFrame }))
|
|
|
|
/**
|
|
* Executes the given function without capturing any parents in the current capture context.
|
|
*
|
|
* This is mainly useful if you want to run an effect only when certain signals change while also
|
|
* dereferencing other signals which should not cause the effect to rerun on their own.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const name = atom('name', 'Sam')
|
|
* const time = atom('time', () => new Date().getTime())
|
|
*
|
|
* setInterval(() => {
|
|
* time.set(new Date().getTime())
|
|
* })
|
|
*
|
|
* react('log name changes', () => {
|
|
* print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))
|
|
* })
|
|
*
|
|
* ```
|
|
*
|
|
* @public
|
|
*/
|
|
export function unsafe__withoutCapture<T>(fn: () => T): T {
|
|
const oldStack = inst.stack
|
|
inst.stack = null
|
|
try {
|
|
return fn()
|
|
} finally {
|
|
inst.stack = oldStack
|
|
}
|
|
}
|
|
|
|
export function startCapturingParents(child: Child) {
|
|
inst.stack = new CaptureStackFrame(inst.stack, child)
|
|
}
|
|
|
|
export function stopCapturingParents() {
|
|
const frame = inst.stack!
|
|
inst.stack = frame.below
|
|
|
|
const didParentsChange = frame.numNewParents > 0 || frame.offset !== frame.child.parents.length
|
|
|
|
if (!didParentsChange) {
|
|
return
|
|
}
|
|
|
|
for (let i = frame.offset; i < frame.child.parents.length; i++) {
|
|
const p = frame.child.parents[i]
|
|
const parentWasRemoved = frame.child.parents.indexOf(p) >= frame.offset
|
|
if (parentWasRemoved) {
|
|
detach(p, frame.child)
|
|
}
|
|
}
|
|
|
|
frame.child.parents.length = frame.offset
|
|
frame.child.parentEpochs.length = frame.offset
|
|
|
|
if (inst.stack?.maybeRemoved) {
|
|
for (let i = 0; i < inst.stack.maybeRemoved.length; i++) {
|
|
const maybeRemovedParent = inst.stack.maybeRemoved[i]
|
|
if (frame.child.parents.indexOf(maybeRemovedParent) === -1) {
|
|
detach(maybeRemovedParent, frame.child)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// this must be called after the parent is up to date
|
|
export function maybeCaptureParent(p: Signal<any, any>) {
|
|
if (inst.stack) {
|
|
const idx = inst.stack.child.parents.indexOf(p)
|
|
// if the child didn't deref this parent last time it executed, then idx will be -1
|
|
// if the child did deref this parent last time but in a different order relative to other parents, then idx will be greater than stack.offset
|
|
// if the child did deref this parent last time in the same order, then idx will be the same as stack.offset
|
|
// if the child did deref this parent already during this capture session then 0 <= idx < stack.offset
|
|
|
|
if (idx < 0) {
|
|
inst.stack.numNewParents++
|
|
if (inst.stack.child.isActivelyListening) {
|
|
attach(p, inst.stack.child)
|
|
}
|
|
}
|
|
|
|
if (idx < 0 || idx >= inst.stack.offset) {
|
|
if (idx !== inst.stack.offset && idx > 0) {
|
|
const maybeRemovedParent = inst.stack.child.parents[inst.stack.offset]
|
|
|
|
if (!inst.stack.maybeRemoved) {
|
|
inst.stack.maybeRemoved = [maybeRemovedParent]
|
|
} else if (inst.stack.maybeRemoved.indexOf(maybeRemovedParent) === -1) {
|
|
inst.stack.maybeRemoved.push(maybeRemovedParent)
|
|
}
|
|
}
|
|
|
|
inst.stack.child.parents[inst.stack.offset] = p
|
|
inst.stack.child.parentEpochs[inst.stack.offset] = p.lastChangedEpoch
|
|
inst.stack.offset++
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A debugging tool that tells you why a computed signal or effect is running.
|
|
* Call in the body of a computed signal or effect function.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const name = atom('name', 'Bob')
|
|
* react('greeting', () => {
|
|
* whyAmIRunning()
|
|
* print('Hello', name.get())
|
|
* })
|
|
*
|
|
* name.set('Alice')
|
|
*
|
|
* // 'greeting' is running because:
|
|
* // 'name' changed => 'Alice'
|
|
* ```
|
|
*
|
|
* @public
|
|
*/
|
|
export function whyAmIRunning() {
|
|
const child = inst.stack?.child
|
|
if (!child) {
|
|
throw new Error('whyAmIRunning() called outside of a reactive context')
|
|
}
|
|
|
|
const changedParents = []
|
|
for (let i = 0; i < child.parents.length; i++) {
|
|
const parent = child.parents[i]
|
|
|
|
if (parent.lastChangedEpoch > child.parentEpochs[i]) {
|
|
changedParents.push(parent)
|
|
}
|
|
}
|
|
|
|
if (changedParents.length === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log((child as any).name, 'is running but none of the parents changed')
|
|
} else {
|
|
// eslint-disable-next-line no-console
|
|
console.log((child as any).name, 'is running because:')
|
|
for (const changedParent of changedParents) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
'\t',
|
|
(changedParent as any).name,
|
|
'changed =>',
|
|
changedParent.__unsafe__getWithoutCapture(true)
|
|
)
|
|
}
|
|
}
|
|
}
|