VulpesOS/gaia/apps/music/components/dom-scheduler/lib/dom-scheduler.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

338 lines
8.7 KiB
JavaScript
Executable file

/* global performance */
(function(exports) {
'use strict';
var debug = false;
function log(str) {
if (!debug) {
return;
}
console.log('🎶 ', str);
}
// The naive mode just executes all blocks right away
// (useful for performance comparison).
var naive = false;
var naiveExec = function(block) {
block();
return Promise.resolve();
};
var directProtectionWindow = 360;
var Scheduler = function() {
this._directTracking = new Map();
this._ongoingTransitions = 0;
this._queuedTransitions = [];
this._flushing = false;
this._pendingMutations = [];
};
Scheduler.prototype = {
// *Direct* blocks should be used for direct manipulation use cases
// (touchevents, scrollevents...).
// They're exectuted in a requestAnimationFrame block and are protected
// from mutations. Request might be cancelled by subsequent direct blocks if
// the event loop gets too busy.
attachDirect: function(elm, evtType, block) {
var tracking = this._directTracking;
if (!tracking.get(elm)) {
tracking.set(elm, {});
}
var tElm = tracking.get(elm);
if (!(evtType in tElm)) {
tElm[evtType] = {
rafID: null,
protectionTimeout: null,
blocks: []
};
}
var tEvt = tElm[evtType];
if (!tEvt.blocks.length) {
elm.addEventListener(evtType, this);
}
tEvt.blocks.push(block);
},
detachDirect: function(elm, evtType, block) {
var tEvt = this._trackingFor(elm, evtType);
if (!tEvt) {
return;
}
if (tEvt.rafID) {
window.cancelAnimationFrame(tEvt.rafID);
}
if (tEvt.protectionTimeout) {
clearTimeout(tEvt.protectionTimeout);
tEvt.protectionTimeout = null;
this._stopProtectingDirect();
}
var toRemove = tEvt.blocks.indexOf(block);
if (toRemove === -1) {
log('wrong detach block');
return;
}
tEvt.blocks.splice(toRemove, 1);
if (!tEvt.blocks.length) {
elm.removeEventListener(evtType, this);
var tElm = this._directTracking.get(elm);
delete tElm[evtType];
if (Object.keys(tElm).length === 0) {
this._directTracking.delete(elm);
}
}
},
_trackingFor: function(elm, evtType) {
var tracking = this._directTracking;
if (!tracking.get(elm)) {
log('wrong detach element');
return;
}
var tElm = tracking.get(elm);
if (!(evtType in tElm)) {
log('wrong detach event');
return;
}
return tElm[evtType];
},
_shouldProtectDirect: function() {
var tracking = this._directTracking;
for (var elm of tracking.keys()) {
var tElm = tracking.get(elm);
for (var evtType in tElm) {
var tEvt = tElm[evtType];
if (tEvt.protectionTimeout !== null) {
return true;
}
}
}
return false;
},
_stopProtectingDirect: function() {
this._dequeueTransitions();
this._flushMutations();
if (debug) {
console.timeEnd('protecting');
}
},
handleEvent: function(evt) {
var tEvt = this._trackingFor(evt.currentTarget, evt.type);
if (!tEvt) {
return;
}
if (naive) {
tEvt.blocks.forEach(function(block) {
block(evt);
});
return;
}
if (tEvt.protectionTimeout) {
clearTimeout(tEvt.protectionTimeout);
} else {
if (debug) {
console.time('protecting');
}
}
tEvt.protectionTimeout = setTimeout((function() {
tEvt.protectionTimeout = null;
this._stopProtectingDirect();
}).bind(this), directProtectionWindow);
if (tEvt.rafID) {
window.cancelAnimationFrame(tEvt.rafID);
}
tEvt.rafID = window.requestAnimationFrame(function() {
tEvt.blocks.forEach(function(block) {
var startDate;
if (debug) {
startDate = performance.now();
}
block(evt);
if (debug) {
var blockDuration = performance.now() - startDate;
if (blockDuration > 16) {
log('Direct block took more than a frame (' +
blockDuration.toString() + 'ms)');
}
}
});
});
},
// *Feedbacks* blocks have a built in 'transitionend' wait mechanism.
// They're protected from mutation and will be delayed during a mutation
// flush.
// They have the same priority as `direct` blocks.
//
// -> Returns a promise fullfilled at the end of the transition for chaining
feedback: function(block, elm, evt, timeout) {
return this._transition(block, true, elm, evt, timeout);
},
// *Transitions* blocks have a built in 'transitionend' wait mechanism.
// They're protected from mutation and will be delayed during a mutation
// flush.
// They will also be delayed by `direct` blocks.
//
// -> Returns a promise fullfilled at the end of the transition for chaining
transition: function(block, elm, evt, timeout) {
return this._transition(block, false, elm, evt, timeout);
},
_transition: function(block, feedback, elm, evt, timeout) {
if (naive) {
return naiveExec(block);
}
timeout = timeout || 500;
return new Promise((function(resolve, reject) {
var content = (function() {
this._ongoingTransitions++;
block();
if (!elm || !evt) {
this._ongoingTransitions--;
resolve();
return;
}
if (debug) {
console.time('animating');
}
var finishTimeout;
var done = (function() {
clearTimeout(finishTimeout);
elm.removeEventListener(evt, done);
this._ongoingTransitions--;
if (debug) {
console.timeEnd('animating');
}
if (this._ongoingTransitions === 0) {
setTimeout(this._flushMutations.bind(this));
}
resolve();
}).bind(this);
elm.addEventListener(evt, done);
finishTimeout = setTimeout(function() {
done();
log('Transition block saved by a timeout of ' + timeout + ' ~ ' +
elm.style.transition);
}, timeout);
}).bind(this);
if (this._flushing || (this._shouldProtectDirect() && !feedback)) {
this._queuedTransitions.push(content);
} else {
content();
}
}).bind(this));
},
_dequeueTransitions: function() {
if (this._queuedTransitions.length === 0) {
return;
}
if (this._flushing || this._shouldProtectDirect()) {
return;
}
var transitions = this._queuedTransitions;
transitions.forEach(function(transition) {
transition();
});
this._queuedTransitions = [];
},
// *Mutations* blocks should be used to write to the DOM or perform
// actions requiring a reflow that are not direct manipulations.
// We shoud always aim for the document to be almost visually identical
// _before_ and _after_ a mutation block.
// Any big change in layout/size will cause a flash/jump.
//
// -> Returns a promise fullfilled after the reflow for chaining
mutation: function(block) {
if (naive) {
return naiveExec(block);
}
return new Promise((function(resolve, reject) {
if (this._shouldProtectDirect() || this._ongoingTransitions > 0) {
this._pendingMutations.push({
block: block,
resolve: resolve
});
} else {
Promise.resolve()
.then(block)
.then(resolve);
}
}).bind(this));
},
_flushMutations: function() {
if (this._pendingMutations.length === 0) {
return;
}
if (this._shouldProtectDirect() || this._ongoingTransitions > 0) {
return;
}
this._flushing = true;
var fulfilments =
this._pendingMutations
.map(function(obj) { return obj.resolve; });
var mutations = this._pendingMutations;
mutations.forEach(function(mutation) {
mutation.block();
});
this._pendingMutations = [];
this._flushing = false;
fulfilments.forEach(function(resolve) { resolve(); });
this._dequeueTransitions();
}
};
exports.DomScheduler = Scheduler;
// We only ever want there to be
// one instance of the scheduler
if (!exports.scheduler || typeof exports.scheduler.mutation !== 'function') Object.defineProperty(exports, 'scheduler', {value:new Scheduler(),configurable:true,writable:true});
})(window);