'rev-deleted-event' => $revDeletedMsg,
'tableClasses' => $tableClasses,
'timestamp' => $block[0]->timestamp,
+ 'fullTimestamp' => $block[0]->getAttribute( 'rc_timestamp' ),
'users' => $usersList,
];
}
$cCache = ObjectCache::getLocalClusterInstance();
if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
- $lbConf['memCache'] = $cCache;
+ $lbConf['memStash'] = $cCache;
}
$wCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
InfoAction::invalidateCache( $title );
+ // Commit any writes here in case this method is called in a loop.
+ // In that case, the scoped lock will fail to be acquired.
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+
return true;
}
* - localDomain: A DatabaseDomain or domain ID string.
* - readOnlyReason : Reason the master DB is read-only if so [optional]
* - srvCache : BagOStuff object for server cache [optional]
- * - memCache : BagOStuff object for cluster memory cache [optional]
+ * - memStash : BagOStuff object for cross-datacenter memory storage [optional]
* - wanCache : WANObjectCache object [optional]
* - hostname : The name of the current server [optional]
* - cliMode: Whether the execution context is a CLI script. [optional]
/** @var BagOStuff */
protected $srvCache;
/** @var BagOStuff */
- protected $memCache;
+ protected $memStash;
/** @var WANObjectCache */
protected $wanCache;
}
$this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff();
- $this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff();
+ $this->memStash = isset( $conf['memStash'] ) ? $conf['memStash'] : new EmptyBagOStuff();
$this->wanCache = isset( $conf['wanCache'] )
? $conf['wanCache']
: WANObjectCache::newEmpty();
}
$this->chronProt = new ChronologyProtector(
- $this->memCache,
+ $this->memStash,
[
'ip' => $this->requestInfo['IPAddress'],
'agent' => $this->requestInfo['UserAgent'],
* - readOnlyReason : Reason the master DB is read-only if so [optional]
* - waitTimeout : Maximum time to wait for replicas for consistency [optional]
* - srvCache : BagOStuff object for server cache [optional]
- * - memCache : BagOStuff object for cluster memory cache [optional]
* - wanCache : WANObjectCache object [optional]
* - chronologyProtector: ChronologyProtector object [optional]
* - hostname : The name of the current server [optional]
private $chronProt;
/** @var BagOStuff */
private $srvCache;
- /** @var BagOStuff */
- private $memCache;
/** @var WANObjectCache */
private $wanCache;
/** @var object|string Class name or object With profileIn/profileOut methods */
} else {
$this->srvCache = new EmptyBagOStuff();
}
- if ( isset( $params['memCache'] ) ) {
- $this->memCache = $params['memCache'];
- } else {
- $this->memCache = new EmptyBagOStuff();
- }
if ( isset( $params['wanCache'] ) ) {
$this->wanCache = $params['wanCache'];
} else {
}
$this->loadMonitor = new $class(
- $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig );
+ $this, $this->srvCache, $this->wanCache, $this->loadMonitorConfig );
$this->loadMonitor->setLogger( $this->replLogger );
}
use Psr\Log\LoggerAwareInterface;
use BagOStuff;
+use WANObjectCache;
/**
* An interface for database load monitoring
*
* @param ILoadBalancer $lb LoadBalancer this instance serves
* @param BagOStuff $sCache Local server memory cache
- * @param BagOStuff $cCache Local cluster memory cache
+ * @param WANObjectCache $wCache Local cluster memory cache
* @param array $options Options map
*/
public function __construct(
- ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+ ILoadBalancer $lb, BagOStuff $sCache, WANObjectCache $wCache, array $options = []
);
/**
use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
use BagOStuff;
+use WANObjectCache;
/**
* Basic DB load monitor with no external dependencies
protected $parent;
/** @var BagOStuff */
protected $srvCache;
- /** @var BagOStuff */
- protected $mainCache;
+ /** @var WANObjectCache */
+ protected $wanCache;
/** @var LoggerInterface */
protected $replLogger;
const VERSION = 1; // cache key version
public function __construct(
- ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+ ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = []
) {
$this->parent = $lb;
$this->srvCache = $srvCache;
- $this->mainCache = $cache;
+ $this->wanCache = $wCache;
$this->replLogger = new NullLogger();
$this->movingAveRatio = isset( $options['movingAveRatio'] )
$staleValue = $value ?: false;
# (b) Check the shared cache and backfill APC
- $value = $this->mainCache->get( $key );
+ $value = $this->wanCache->get( $key );
if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
$this->srvCache->set( $key, $value, $staleTTL );
$this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
$staleValue = $value ?: $staleValue;
# (c) Cache key missing or expired; regenerate and backfill
- if ( $this->mainCache->lock( $key, 0, 10 ) ) {
- # Let this process alone update the cache value
- $cache = $this->mainCache;
+ if ( $this->srvCache->lock( $key, 0, 10 ) ) {
+ # Let only this process update the cache value on this server
+ $sCache = $this->srvCache;
/** @noinspection PhpUnusedLocalVariableInspection */
- $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
- $cache->unlock( $key );
+ $unlocker = new ScopedCallback( function () use ( $sCache, $key ) {
+ $sCache->unlock( $key );
} );
} elseif ( $staleValue ) {
# Could not acquire lock but an old cache exists, so use it
'weightScales' => $weightScales,
'timestamp' => microtime( true )
];
- $this->mainCache->set( $key, $value, $staleTTL );
+ $this->wanCache->set( $key, $value, $staleTTL );
$this->srvCache->set( $key, $value, $staleTTL );
$this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
namespace Wikimedia\Rdbms;
use BagOStuff;
+use WANObjectCache;
/**
* Basic MySQL load monitor with no external dependencies
private $warmCacheRatio;
public function __construct(
- ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+ ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = []
) {
- parent::__construct( $lb, $srvCache, $cache, $options );
+ parent::__construct( $lb, $srvCache, $wCache, $options );
$this->warmCacheRatio = isset( $options['warmCacheRatio'] )
? $options['warmCacheRatio']
use Psr\Log\LoggerInterface;
use BagOStuff;
+use WANObjectCache;
class LoadMonitorNull implements ILoadMonitor {
public function __construct(
- ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+ ILoadBalancer $lb, BagOStuff $sCache, WANObjectCache $wCache, array $options = []
) {
}
),
// uses 'watch' or 'unwatch' message
'text' => $this->msg( $mode )->text(),
- 'href' => $title->getLocalURL( [ 'action' => $mode ] )
+ 'href' => $title->getLocalURL( [ 'action' => $mode ] ),
+ // Set a data-mw=interface attribute, which the mediawiki.page.ajax
+ // module will look for to make sure it's a trusted link
+ 'data' => [
+ 'mw' => 'interface',
+ ],
];
}
}
-<table class="{{# tableClasses }}{{ . }} {{/ tableClasses }}">
+<table class="{{# tableClasses }}{{ . }} {{/ tableClasses }}" data-mw-ts="{{{ fullTimestamp }}}">
<tr>
<td>
<span class="mw-collapsible-toggle mw-collapsible-arrow mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>
"rcfilters-restore-default-filters": "Restore default filters",
"rcfilters-clear-all-filters": "Clear all filters",
"rcfilters-show-new-changes": "View newest changes",
- "rcfilters-previous-changes-label": "Previously viewed changes",
"rcfilters-search-placeholder": "Filter recent changes (browse or start typing)",
"rcfilters-invalid-filter": "Invalid filter",
"rcfilters-empty-filter": "No active filters. All contributions are shown.",
"rcfilters-filterlist-noresults": "No filters found",
"rcfilters-noresults-conflict": "No results found because the search criteria are in conflict",
"rcfilters-state-message-subset": "This filter has no effect because its results are included with those of the following, broader {{PLURAL:$2|filter|filters}} (try highlighting to distinguish it): $1",
- "rcfilters-state-message-fullcoverage": "Selecting all filters in a group is the same as selecting none, so this filter has no effect. Group includes: $1",
+ "rcfilters-state-message-fullcoverage": "Selecting all filters in this group is the same as selecting none, so this filter has no effect. Group includes: $1",
"rcfilters-filtergroup-authorship": "Contribution authorship",
"rcfilters-filter-editsbyself-label": "Changes by you",
"rcfilters-filter-editsbyself-description": "Your own contributions.",
"rcfilters-restore-default-filters": "Label for the button that resets filters to defaults",
"rcfilters-clear-all-filters": "Title for the button that clears all filters",
"rcfilters-show-new-changes": "Label for the button to show new changes.",
- "rcfilters-previous-changes-label": "Label to indicate the changes below have been previously viewed.",
"rcfilters-search-placeholder": "Placeholder for the filter search input.",
"rcfilters-invalid-filter": "A label for an invalid filter.",
"rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
'rcfilters-restore-default-filters',
'rcfilters-clear-all-filters',
'rcfilters-show-new-changes',
- 'rcfilters-previous-changes-label',
'rcfilters-search-placeholder',
'rcfilters-invalid-filter',
'rcfilters-empty-filter',
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
// Update state
var changed = false,
- active = this.areAnySelected();
-
- if (
- item.isSelected() &&
- this.getType() === 'single_option' &&
- this.currSelected &&
- this.currSelected !== item
- ) {
- this.currSelected.toggleSelected( false );
- }
-
- // For 'single_option' groups, check if we just unselected all
- // items. This should never be the result. If we did unselect
- // all (like resetting all filters to false) then this group
- // must choose its default item or the first item in the group
- if (
- this.getType() === 'single_option' &&
- !this.getItems().some( function ( filterItem ) {
- return filterItem.isSelected();
- } )
- ) {
- // Single option means there must be a single option
- // selected, so we have to either select the default
- // or select the first option
- this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
- this.getItems()[ 0 ];
- this.currSelected.toggleSelected( true );
- changed = true;
+ active = this.areAnySelected(),
+ model = this;
+
+ if ( this.getType() === 'single_option' ) {
+ // This group must have one item selected always
+ // and must never have more than one item selected at a time
+ if ( this.getSelectedItems().length === 0 ) {
+ // Nothing is selected anymore
+ // Select the default or the first item
+ this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+ this.getItems()[ 0 ];
+ this.currSelected.toggleSelected( true );
+ changed = true;
+ } else if ( this.getSelectedItems().length > 1 ) {
+ // There is more than one item selected
+ // This should only happen if the item given
+ // is the one that is selected, so unselect
+ // all items that is not it
+ this.getSelectedItems().forEach( function ( itemModel ) {
+ // Note that in case the given item is actually
+ // not selected, this loop will end up unselecting
+ // all items, which would trigger the case above
+ // when the last item is unselected anyways
+ var selected = itemModel.getName() === item.getName() &&
+ item.isSelected();
+
+ itemModel.toggleSelected( selected );
+ if ( selected ) {
+ model.currSelected = itemModel;
+ }
+ } );
+ changed = true;
+ }
}
if (
@import 'mw.rcfilters.mixins';
+@keyframes fadeBlue {
+ 60% {
+ border-top-color: #36c;
+ }
+ 100% {
+ border-top-color: #c8ccd1;
+ }
+}
+
.mw-rcfilters-ui-changesListWrapperWidget {
&-newChanges {
&-previousChangesIndicator {
margin: 10px 0;
- color: #36c;
- border-top: 2px solid #36c;
- text-align: center;
-
- &:hover {
- color: #72777d;
- border-top-color: #72777d;
- cursor: pointer;
- }
+ border-top: 2px solid #c8ccd1;
+ animation: 1s ease fadeBlue;
}
&-results {
$message = $( '<div>' )
.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
isEmpty = $changesListContent === 'NO_RESULTS',
- $lastSeen,
- $indicator,
- $newChanges = $( [] ),
// For enhanced mode, we have to load these modules, which are
// not loaded for the 'regular' mode in the backend
loaderPromise = mw.user.options.get( 'usenewrc' ) ?
this.$element.empty().append( $changesListContent );
if ( from ) {
- $lastSeen = null;
- this.$element.find( 'li[data-mw-ts]' ).each( function () {
- var $li = $( this ),
- ts = $li.data( 'mw-ts' );
-
- if ( ts >= from ) {
- $newChanges = $newChanges.add( $li );
- } else if ( $lastSeen === null ) {
- $lastSeen = $li;
- return false;
- }
- } );
-
- if ( $lastSeen ) {
- $indicator = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' )
- .text( mw.message( 'rcfilters-previous-changes-label' ).text() );
-
- $indicator.on( 'click', function () {
- $indicator.detach();
- } );
-
- $lastSeen.before( $indicator );
- }
-
- $newChanges
- .hide()
- .fadeIn( 1000 );
+ this.emphasizeNewChanges( from );
}
}
} );
};
+ /**
+ * Emphasize the elements (or groups) newer than the 'from' parameter
+ * @param {string} from Anything newer than this is considered 'new'
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
+ var $firstNew,
+ $indicator,
+ $newChanges = $( [] ),
+ selector = this.inEnhancedMode() ?
+ 'table.mw-enhanced-rc[data-mw-ts]' :
+ 'li[data-mw-ts]',
+ set = this.$element.find( selector ),
+ length = set.length;
+
+ set.each( function ( index ) {
+ var $this = $( this ),
+ ts = $this.data( 'mw-ts' );
+
+ if ( ts >= from ) {
+ $newChanges = $newChanges.add( $this );
+ $firstNew = $this;
+
+ // guards against putting the marker after the last element
+ if ( index === ( length - 1 ) ) {
+ $firstNew = null;
+ }
+ }
+ } );
+
+ if ( $firstNew ) {
+ $indicator = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
+
+ $firstNew.after( $indicator );
+ }
+
+ $newChanges
+ .hide()
+ .fadeIn( 1000 );
+ };
+
/**
* Respond to changes list model newChangesExist
*
* @param {jQuery|string} $content The content of the updated changes list
*/
mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
- var uri = new mw.Uri(),
- highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
+ var highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
$highlights = $( '<div>' )
.addClass( highlightClass )
.append(
);
} );
- if (
- ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
- ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) )
- ) {
+ if ( this.inEnhancedMode() ) {
// Enhanced RC
$content.find( 'td.mw-enhanced-rc' )
.parent()
}
};
+ /**
+ * @return {boolean} Whether the changes are grouped by page
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
+ var uri = new mw.Uri();
+ return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
+ ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
+ };
+
/**
* Apply color classes based on filters highlight configuration
*/
);
$( function () {
- var $links = $( '.mw-watchlink a, a.mw-watchlink' );
- // Restrict to core interfaces, ignore user-generated content
- $links = $links.filter( ':not( #bodyContent *, #content * )' );
+ var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
+ if ( !$links.length ) {
+ // Fallback to the class-based exclusion method for backwards-compatibility
+ $links = $( '.mw-watchlink a, a.mw-watchlink' );
+ // Restrict to core interfaces, ignore user-generated content
+ $links = $links.filter( ':not( #bodyContent *, #content * )' );
+ }
$links.click( function ( e ) {
var mwTitle, action, api, $link;