@ -5,12 +5,18 @@ import {
BOUNDARY _EFFECT ,
BRANCH _EFFECT ,
CLEAN ,
CONNECTED ,
DERIVED ,
DIRTY ,
EFFECT ,
ASYNC ,
DESTROYED ,
INERT ,
MAYBE _DIRTY ,
RENDER _EFFECT ,
ROOT _EFFECT
ROOT _EFFECT ,
WAS _MARKED ,
MANAGED _EFFECT
} from '#client/constants' ;
import { snapshot } from '../../shared/clone.js' ;
import { untrack } from '../runtime.js' ;
@ -30,11 +36,13 @@ export function root(effect) {
/ * *
*
* @ param { Effect } effect
* @ param { boolean } append _effect
* @ returns { string }
* /
export function log _effect _tree ( effect , depth = 0 ) {
function effect _label ( effect , append _effect = false ) {
const flags = effect . f ;
let label = '(unknown)' ;
let label = ` (unknown ${ append _effect ? 'effect' : '' } ) ` ;
if ( ( flags & ROOT _EFFECT ) !== 0 ) {
label = 'root' ;
@ -42,6 +50,8 @@ export function log_effect_tree(effect, depth = 0) {
label = 'boundary' ;
} else if ( ( flags & BLOCK _EFFECT ) !== 0 ) {
label = 'block' ;
} else if ( ( flags & MANAGED _EFFECT ) !== 0 ) {
label = 'managed' ;
} else if ( ( flags & ASYNC ) !== 0 ) {
label = 'async' ;
} else if ( ( flags & BRANCH _EFFECT ) !== 0 ) {
@ -52,6 +62,20 @@ export function log_effect_tree(effect, depth = 0) {
label = 'effect' ;
}
if ( append _effect && ! label . endsWith ( 'effect' ) ) {
label += ' effect' ;
}
return label ;
}
/ * *
*
* @ param { Effect } effect
* /
export function log _effect _tree ( effect , depth = 0 ) {
const flags = effect . f ;
const label = effect _label ( effect ) ;
let status =
( flags & CLEAN ) !== 0 ? 'clean' : ( flags & MAYBE _DIRTY ) !== 0 ? 'maybe dirty' : 'dirty' ;
@ -140,3 +164,337 @@ function log_dep(dep) {
) ;
}
}
/ * *
* Logs all reactions of a source or derived transitively
* @ param { Derived | Value } signal
* /
export function log _reactions ( signal ) {
/** @type {Set<Derived | Value>} */
const visited = new Set ( ) ;
/ * *
* Returns an array of flag names that are set on the given flags bitmask
* @ param { number } flags
* @ returns { string [ ] }
* /
function get _derived _flag _names ( flags ) {
/** @type {string[]} */
const names = [ ] ;
if ( ( flags & CLEAN ) !== 0 ) names . push ( 'CLEAN' ) ;
if ( ( flags & DIRTY ) !== 0 ) names . push ( 'DIRTY' ) ;
if ( ( flags & MAYBE _DIRTY ) !== 0 ) names . push ( 'MAYBE_DIRTY' ) ;
if ( ( flags & CONNECTED ) !== 0 ) names . push ( 'CONNECTED' ) ;
if ( ( flags & WAS _MARKED ) !== 0 ) names . push ( 'WAS_MARKED' ) ;
if ( ( flags & INERT ) !== 0 ) names . push ( 'INERT' ) ;
if ( ( flags & DESTROYED ) !== 0 ) names . push ( 'DESTROYED' ) ;
return names ;
}
/ * *
* @ param { Derived | Value } d
* @ param { number } depth
* /
function log _derived ( d , depth ) {
const flags = d . f ;
const flag _names = get _derived _flag _names ( flags ) ;
const flags _str = flag _names . length > 0 ? ` ( ${ flag _names . join ( ', ' ) } ) ` : '(no flags)' ;
// eslint-disable-next-line no-console
console . group (
` %c ${ flags & DERIVED ? '$derived' : '$state' } %c ${ d . label ? ? '<unknown>' } %c ${ flags _str } ` ,
'font-weight: bold; color: CornflowerBlue' ,
'font-weight: normal; color: inherit' ,
'font-weight: normal; color: gray'
) ;
// eslint-disable-next-line no-console
console . log ( untrack ( ( ) => snapshot ( d . v ) ) ) ;
if ( 'fn' in d ) {
// eslint-disable-next-line no-console
console . log ( '%cfn:' , 'font-weight: bold' , d . fn ) ;
}
if ( d . reactions !== null && d . reactions . length > 0 ) {
// eslint-disable-next-line no-console
console . group ( '%creactions' , 'font-weight: bold' ) ;
for ( const reaction of d . reactions ) {
if ( ( reaction . f & DERIVED ) !== 0 ) {
const derived _reaction = /** @type {Derived} */ ( reaction ) ;
if ( visited . has ( derived _reaction ) ) {
// eslint-disable-next-line no-console
console . log (
` %c $ derived %c ${ derived _reaction . label ? ? '<unknown>' } %c(already seen) ` ,
'font-weight: bold; color: CornflowerBlue' ,
'font-weight: normal; color: inherit' ,
'font-weight: bold; color: orange'
) ;
} else {
visited . add ( derived _reaction ) ;
log _derived ( derived _reaction , depth + 1 ) ;
}
} else {
// It's an effect
const label = effect _label ( /** @type {Effect} */ ( reaction ) , true ) ;
const status = ( flags & MAYBE _DIRTY ) !== 0 ? 'maybe dirty' : 'dirty' ;
// Collect parent statuses
/** @type {string[]} */
const parent _statuses = [ ] ;
let show = false ;
let current = /** @type {Effect} */ ( reaction ) . parent ;
while ( current !== null ) {
const parent _flags = current . f ;
if ( ( parent _flags & ( ROOT _EFFECT | BRANCH _EFFECT ) ) !== 0 ) {
const parent _status = ( parent _flags & CLEAN ) !== 0 ? 'clean' : 'not clean' ;
if ( parent _status === 'clean' && parent _statuses . includes ( 'not clean' ) ) show = true ;
parent _statuses . push ( parent _status ) ;
}
if ( ! current . parent ) break ;
current = current . parent ;
}
// Check if reaction is reachable from root
const seen _effects = new Set ( ) ;
let reachable = false ;
/ * *
* @ param { Effect | null } effect
* /
function check _reachable ( effect ) {
if ( effect === null || reachable ) return ;
if ( effect === reaction ) {
reachable = true ;
return ;
}
if ( effect . f & DESTROYED ) return ;
if ( seen _effects . has ( effect ) ) {
throw new Error ( '' ) ;
}
seen _effects . add ( effect ) ;
let child = effect . first ;
while ( child !== null ) {
check _reachable ( child ) ;
child = child . next ;
}
}
try {
if ( current ) check _reachable ( current ) ;
} catch ( e ) {
// eslint-disable-next-line no-console
console . log (
` %c⚠️ Circular reference detected in effect tree ` ,
'font-weight: bold; color: red' ,
seen _effects
) ;
}
if ( ! reachable ) {
// eslint-disable-next-line no-console
console . log (
` %c⚠️ Effect is NOT reachable from its parent chain ` ,
'font-weight: bold; color: red'
) ;
}
const parent _status _str = show ? ` ( ${ parent _statuses . join ( ', ' ) } ) ` : '' ;
// eslint-disable-next-line no-console
console . log (
` %c ${ label } ( ${ status } ) ${ parent _status _str } ` ,
` font-weight: bold; color: ${ parent _status _str ? 'red' : 'green' } ` ,
reaction
) ;
}
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
} else {
// eslint-disable-next-line no-console
console . log ( '%cno reactions' , 'font-style: italic; color: gray' ) ;
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
}
// eslint-disable-next-line no-console
console . group ( ` %cDerived Reactions Graph ` , 'font-weight: bold; color: purple' ) ;
visited . add ( signal ) ;
log _derived ( signal , 0 ) ;
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
}
/ * *
* Traverses an effect tree and logs branches where a non - clean branch exists below a clean branch
* @ param { Effect } effect
* /
export function log _inconsistent _branches ( effect ) {
const root _effect = root ( effect ) ;
/ * *
* @ typedef { {
* effect : Effect ,
* status : 'clean' | 'maybe dirty' | 'dirty' ,
* parent _clean : boolean ,
* children : BranchInfo [ ]
* } } BranchInfo
* /
/ * *
* Collects branch effects from the tree
* @ param { Effect } eff
* @ param { boolean } parent _clean - whether any ancestor branch is clean
* @ returns { BranchInfo [ ] }
* /
function collect _branches ( eff , parent _clean ) {
/** @type {BranchInfo[]} */
const branches = [ ] ;
const flags = eff . f ;
const is _branch = ( flags & BRANCH _EFFECT ) !== 0 ;
if ( is _branch ) {
const status =
( flags & CLEAN ) !== 0 ? 'clean' : ( flags & MAYBE _DIRTY ) !== 0 ? 'maybe dirty' : 'dirty' ;
/** @type {BranchInfo[]} */
const child _branches = [ ] ;
let child = eff . first ;
while ( child !== null ) {
child _branches . push ( ... collect _branches ( child , status === 'clean' ) ) ;
child = child . next ;
}
branches . push ( {
effect : eff ,
status ,
parent _clean ,
children : child _branches
} ) ;
} else {
// Not a branch, continue traversing
let child = eff . first ;
while ( child !== null ) {
branches . push ( ... collect _branches ( child , parent _clean ) ) ;
child = child . next ;
}
}
return branches ;
}
/ * *
* Checks if a branch tree contains any inconsistencies ( non - clean below clean )
* @ param { BranchInfo } branch
* @ param { boolean } ancestor _clean
* @ returns { boolean }
* /
function has _inconsistency ( branch , ancestor _clean ) {
const is _inconsistent = ancestor _clean && branch . status !== 'clean' ;
if ( is _inconsistent ) return true ;
const new _ancestor _clean = ancestor _clean || branch . status === 'clean' ;
for ( const child of branch . children ) {
if ( has _inconsistency ( child , new _ancestor _clean ) ) return true ;
}
return false ;
}
/ * *
* Logs a branch and its children , but only if there are inconsistencies
* @ param { BranchInfo } branch
* @ param { boolean } ancestor _clean
* @ param { number } depth
* /
function log _branch ( branch , ancestor _clean , depth ) {
const is _inconsistent = ancestor _clean && branch . status !== 'clean' ;
const new _ancestor _clean = ancestor _clean || branch . status === 'clean' ;
// Only log if this branch or any descendant has an inconsistency
if ( ! has _inconsistency ( branch , ancestor _clean ) && ! is _inconsistent ) {
return ;
}
const style = is _inconsistent
? 'font-weight: bold; color: red'
: branch . status === 'clean'
? 'font-weight: normal; color: green'
: 'font-weight: bold; color: orange' ;
const warning = is _inconsistent ? ' ⚠️ INCONSISTENT' : '' ;
// eslint-disable-next-line no-console
console . group ( ` %cbranch ( ${ branch . status } ) ${ warning } ` , style ) ;
// eslint-disable-next-line no-console
console . log ( '%ceffect:' , 'font-weight: bold' , branch . effect ) ;
if ( branch . effect . fn ) {
// eslint-disable-next-line no-console
console . log ( '%cfn:' , 'font-weight: bold' , branch . effect . fn ) ;
}
if ( branch . effect . deps !== null ) {
// eslint-disable-next-line no-console
console . groupCollapsed ( '%cdeps' , 'font-weight: normal' ) ;
for ( const dep of branch . effect . deps ) {
log _dep ( dep ) ;
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
}
if ( is _inconsistent ) {
log _effect _tree ( branch . effect ) ;
} else if ( branch . children . length > 0 ) {
// eslint-disable-next-line no-console
console . group ( '%cchild branches' , 'font-weight: bold' ) ;
for ( const child of branch . children ) {
log _branch ( child , new _ancestor _clean , depth + 1 ) ;
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
}
const branches = collect _branches ( root _effect , false ) ;
// Check if there are any inconsistencies at all
let has _any _inconsistency = false ;
for ( const branch of branches ) {
if ( has _inconsistency ( branch , false ) ) {
has _any _inconsistency = true ;
break ;
}
}
if ( ! has _any _inconsistency ) {
// eslint-disable-next-line no-console
console . log ( '%cNo inconsistent branches found' , 'font-weight: bold; color: green' ) ;
return ;
}
// eslint-disable-next-line no-console
console . group ( ` %cInconsistent Branches (non-clean below clean) ` , 'font-weight: bold; color: red' ) ;
for ( const branch of branches ) {
log _branch ( branch , false , 0 ) ;
}
// eslint-disable-next-line no-console
console . groupEnd ( ) ;
return true ;
}