snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
275
packages/state/src/lib/capture.ts
Normal file
275
packages/state/src/lib/capture.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { attach, detach, singleton } from './helpers'
|
||||
import { isComputed } from './isComputed'
|
||||
import type { Child, Signal } from './types'
|
||||
|
||||
class CaptureStackFrame {
|
||||
offset = 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', Date.now())
|
||||
*
|
||||
* setInterval(() => {
|
||||
* time.set(Date.now())
|
||||
* })
|
||||
*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins capturing parent signal dependencies for the given child signal.
|
||||
*
|
||||
* This function initiates a capture session where any signal accessed via `.get()`
|
||||
* will be automatically registered as a dependency of the child signal. It sets up
|
||||
* the capture stack frame and clears the existing parent set to prepare for fresh
|
||||
* dependency tracking.
|
||||
*
|
||||
* @param child - The child signal (computed or effect) that will capture dependencies
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const effect = createEffect('myEffect', () => { /* ... *\/ })
|
||||
* startCapturingParents(effect)
|
||||
* // Now any signal.get() calls will be captured as dependencies
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function startCapturingParents(child: Child) {
|
||||
inst.stack = new CaptureStackFrame(inst.stack, child)
|
||||
if (child.__debug_ancestor_epochs__) {
|
||||
const previousAncestorEpochs = child.__debug_ancestor_epochs__
|
||||
child.__debug_ancestor_epochs__ = null
|
||||
for (const p of child.parents) {
|
||||
p.__unsafe__getWithoutCapture(true)
|
||||
}
|
||||
logChangedAncestors(child, previousAncestorEpochs)
|
||||
}
|
||||
child.parentSet.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the parent dependency capture session and finalizes the dependency graph.
|
||||
*
|
||||
* This function cleans up the capture session by removing dependencies that are no
|
||||
* longer needed, detaching signals that should no longer be parents, and updating
|
||||
* the dependency arrays to reflect the current set of captured parents. It must be
|
||||
* called after `startCapturingParents` to complete the capture cycle.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* startCapturingParents(effect)
|
||||
* // ... signal.get() calls happen here ...
|
||||
* stopCapturingParents() // Finalizes the dependency graph
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function stopCapturingParents() {
|
||||
const frame = inst.stack!
|
||||
inst.stack = frame.below
|
||||
const { child, offset, maybeRemoved } = frame
|
||||
|
||||
if (offset < child.parents.length) {
|
||||
for (let i = offset; i < child.parents.length; i++) {
|
||||
const maybeRemovedParent = child.parents[i]
|
||||
if (!child.parentSet.has(maybeRemovedParent)) {
|
||||
detach(maybeRemovedParent, child)
|
||||
}
|
||||
}
|
||||
|
||||
child.parents.length = offset
|
||||
child.parentEpochs.length = offset
|
||||
}
|
||||
|
||||
if (maybeRemoved) {
|
||||
for (let i = 0; i < maybeRemoved.length; i++) {
|
||||
const maybeRemovedParent = maybeRemoved[i]
|
||||
if (!child.parentSet.has(maybeRemovedParent)) {
|
||||
detach(maybeRemovedParent, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child.__debug_ancestor_epochs__) {
|
||||
captureAncestorEpochs(child, child.__debug_ancestor_epochs__)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally captures a signal as a parent dependency during an active capture session.
|
||||
*
|
||||
* This function is called whenever a signal's `.get()` method is invoked during a
|
||||
* capture session. It checks if the signal should be added as a dependency and manages
|
||||
* the parent-child relationship in the reactive graph. The function handles deduplication,
|
||||
* attachment/detachment, and tracks changes in dependency order.
|
||||
*
|
||||
* Note: This must be called after the parent signal is up to date.
|
||||
*
|
||||
* @param p - The signal that might be captured as a parent dependency
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // This is called internally when you do:
|
||||
* const value = someAtom.get() // maybeCaptureParent(someAtom) is called
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function maybeCaptureParent(p: Signal<any, any>) {
|
||||
const stack = inst.stack
|
||||
if (!stack) return
|
||||
const { child } = stack
|
||||
|
||||
// `add` returns false when the parent was already captured this run. In array mode both
|
||||
// `has` and `add` scan with indexOf, so going straight to `add` halves the scans per
|
||||
// captured parent.
|
||||
if (!child.parentSet.add(p)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (child.isActivelyListening) {
|
||||
attach(p, child)
|
||||
}
|
||||
|
||||
// Parents are recorded in deref order. If the slot at this offset held a different parent
|
||||
// last run, that parent may have moved later in the order or been dropped; only
|
||||
// stopCapturingParents can tell, so remember it for then.
|
||||
if (stack.offset < child.parents.length) {
|
||||
const maybeRemovedParent = child.parents[stack.offset]
|
||||
if (maybeRemovedParent !== p) {
|
||||
if (!stack.maybeRemoved) {
|
||||
stack.maybeRemoved = [maybeRemovedParent]
|
||||
} else {
|
||||
stack.maybeRemoved.push(maybeRemovedParent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.parents[stack.offset] = p
|
||||
child.parentEpochs[stack.offset] = p.lastChangedEpoch
|
||||
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. Nothing is logged for the run that
|
||||
* calls it; from the next run on, each run logs the ancestors that changed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const name = atom('name', 'Bob')
|
||||
* react('greeting', () => {
|
||||
* whyAmIRunning()
|
||||
* print('Hello', name.get())
|
||||
* })
|
||||
*
|
||||
* name.set('Alice')
|
||||
*
|
||||
* // Effect(greeting) is executing because:
|
||||
* // ↳ Atom(name) changed
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function whyAmIRunning() {
|
||||
const child = inst.stack?.child
|
||||
if (!child) {
|
||||
throw new Error('whyAmIRunning() called outside of a reactive context')
|
||||
}
|
||||
child.__debug_ancestor_epochs__ = new Map()
|
||||
}
|
||||
|
||||
function captureAncestorEpochs(child: Child, ancestorEpochs: Map<Signal<any>, number>) {
|
||||
for (let i = 0; i < child.parents.length; i++) {
|
||||
const parent = child.parents[i]
|
||||
ancestorEpochs.set(parent, child.parentEpochs[i])
|
||||
if (isComputed(parent)) {
|
||||
captureAncestorEpochs(parent as any, ancestorEpochs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChangeTree = { [signalName: string]: ChangeTree } | null
|
||||
function collectChangedAncestors(
|
||||
child: Child,
|
||||
ancestorEpochs: Map<Signal<any>, number>
|
||||
): NonNullable<ChangeTree> {
|
||||
const changeTree: ChangeTree = {}
|
||||
for (let i = 0; i < child.parents.length; i++) {
|
||||
const parent = child.parents[i]
|
||||
if (!ancestorEpochs.has(parent)) {
|
||||
continue
|
||||
}
|
||||
if (parent.lastChangedEpoch !== ancestorEpochs.get(parent)) {
|
||||
if (isComputed(parent)) {
|
||||
changeTree[parent.name] = collectChangedAncestors(parent as any, ancestorEpochs)
|
||||
} else {
|
||||
changeTree[parent.name] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
return changeTree
|
||||
}
|
||||
|
||||
function logChangedAncestors(child: Child, ancestorEpochs: Map<Signal<any>, number>) {
|
||||
const changeTree = collectChangedAncestors(child, ancestorEpochs)
|
||||
if (Object.keys(changeTree).length === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Effect(${child.name}) was executed manually.`)
|
||||
return
|
||||
}
|
||||
|
||||
let str = isComputed(child)
|
||||
? `Computed(${child.name}) is recomputing because:`
|
||||
: `Effect(${child.name}) is executing because:`
|
||||
|
||||
function logParent(tree: NonNullable<ChangeTree>, indent: number) {
|
||||
const indentStr = '\n' + ' '.repeat(indent) + '↳ '
|
||||
for (const [name, val] of Object.entries(tree)) {
|
||||
if (val) {
|
||||
str += `${indentStr}Computed(${name}) changed`
|
||||
logParent(val, indent + 2)
|
||||
} else {
|
||||
str += `${indentStr}Atom(${name}) changed`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logParent(changeTree, 1)
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(str)
|
||||
}
|
||||
Reference in New Issue
Block a user