@dark-engine/core
Version:
The lightweight and powerful UI rendering engine without dependencies and written in TypeScript (Browser, Node.js, Android, iOS, Windows, Linux, macOS)
316 lines (315 loc) • 7.96 kB
JavaScript
import { getTime, detectIsPromise, detectIsFunction, dummyFn } from '../utils';
import { HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants';
import { workLoop, detectIsBusy } from '../workloop';
import { EventEmitter } from '../emitter';
import { platform } from '../platform';
class MessageChannel extends EventEmitter {
port1 = null;
port2 = null;
constructor() {
super();
this.port1 = new MessagePort(this);
this.port2 = new MessagePort(this);
}
}
class MessagePort {
channel;
offs = [];
constructor(channel) {
this.channel = channel;
}
on(event, callback) {
const off = this.channel.on(event, callback);
this.offs.push(off);
}
postMessage(value) {
platform.spawn(() => {
this.channel.emit('message', value);
});
}
unref() {
this.offs.forEach(x => x());
this.offs = [];
}
}
class Scheduler {
queue = {
[TaskPriority.HIGH]: [],
[TaskPriority.NORMAL]: [],
[TaskPriority.LOW]: [],
};
batched = [];
deadline = 0;
lastId = 0;
task = null;
scheduledCallback = null;
isMessageLoopRunning = false;
channel = null;
port = null;
constructor() {
this.channel = new MessageChannel();
this.port = this.channel.port2;
this.channel.port1.on('message', this.performWorkUntilDeadline.bind(this));
}
reset() {
this.deadline = 0;
this.task = null;
this.scheduledCallback = null;
this.isMessageLoopRunning = false;
}
shouldYield() {
return getTime() >= this.deadline;
}
schedule(callback, options) {
const task = createTask(callback, options);
if (options.isBatch) {
const { setupBatch: setup } = options;
this.batched.push({ task, setup });
return;
}
this.putAndExecute(task);
}
putAndExecute(task) {
this.lastId = task.getId();
this.put(task);
this.execute();
}
batch() {
const { batched } = this;
const size = batched.length;
for (let i = 0; i < size; i++) {
const { task, setup } = batched[i];
if (i < size - 1) {
detectIsFunction(setup) && setup();
} else {
this.putAndExecute(task);
}
}
this.batched = [];
}
getLastId() {
return this.lastId;
}
detectIsTransition() {
return this.task.getIsTransition();
}
hasNewTask() {
const { high, normal, low } = this.getQueues();
return high.length + normal.length + low.length > 0;
}
retain(fn) {
const { high, normal, low } = this.getQueues();
const tasks = [...high, ...normal, ...low];
const { hasHostUpdate, hasChildUpdate } = collectFlags(this.task, tasks);
if (hasHostUpdate || hasChildUpdate) {
const hasExact = detectHasExact(this.task, tasks);
if (hasExact) {
this.complete(this.task, true);
} else {
this.defer(this.task);
}
this.task.markAsObsolete();
} else {
this.task.setOnRestore(fn);
this.defer(this.task);
}
}
complete(task, isCanceled) {
task.complete(isCanceled);
}
put(task) {
const queue = this.queue[task.getPriority()];
if (task.getIsTransition()) {
const base = task.base();
const tasks = queue.filter(x => x.base() !== base);
queue.splice(0, queue.length, ...tasks);
}
queue.push(task);
}
pick(queue) {
if (queue.length === 0) return false;
this.task = queue.shift();
this.run(this.task);
return true;
}
run(task) {
try {
task.run();
task.getForceAsync() ? this.requestCallbackAsync(workLoop) : this.requestCallback(workLoop);
} catch (something) {
if (detectIsPromise(something)) {
something.catch(dummyFn).finally(() => {
this.run(task);
});
} else {
throw something;
}
}
}
execute() {
const isBusy = detectIsBusy();
if (!isBusy && !this.isMessageLoopRunning) {
const { high, normal, low } = this.getQueues();
this.pick(high) || this.pick(normal) || this.pick(low);
}
}
requestCallbackAsync(callback) {
this.scheduledCallback = callback;
if (!this.isMessageLoopRunning) {
this.isMessageLoopRunning = true;
this.port.postMessage(null);
}
}
requestCallback(callback) {
const something = callback(false);
if (detectIsPromise(something)) {
something.catch(dummyFn).finally(() => {
this.requestCallback(callback);
});
} else {
this.task = null;
this.execute();
}
}
performWorkUntilDeadline() {
if (this.scheduledCallback) {
this.deadline = getTime() + YIELD_INTERVAL;
const something = this.scheduledCallback(true);
if (detectIsPromise(something)) {
something.catch(dummyFn).finally(() => {
this.port.postMessage(null);
});
} else if (something) {
this.port.postMessage(null);
} else {
this.complete(this.task, false);
this.reset();
this.execute();
}
} else {
this.isMessageLoopRunning = false;
}
}
defer(task) {
const { low } = this.getQueues();
low.unshift(task);
}
getQueues() {
const high = this.queue[TaskPriority.HIGH];
const normal = this.queue[TaskPriority.NORMAL];
const low = this.queue[TaskPriority.LOW];
return {
high,
normal,
low,
};
}
}
class Task {
__id;
priority;
forceAsync = false;
isTransition = false;
isObsolete = false;
callback = null;
createLoc = null;
onRestore = null;
onTransitionEnd = null;
static nextTaskId = 0;
constructor(callback, priority, forceAsync) {
this.__id = ++Task.nextTaskId;
this.callback = callback;
this.priority = priority;
this.forceAsync = forceAsync;
}
getId() {
return this.__id;
}
getPriority() {
return this.priority;
}
getForceAsync() {
return this.forceAsync;
}
setIsTransition(x) {
this.isTransition = x;
}
getIsTransition() {
return this.isTransition;
}
run() {
this.isObsolete = false;
this.callback(this.onRestore);
this.onRestore = null;
}
complete(isCanceled) {
this.isTransition &&
!this.isObsolete &&
detectIsFunction(this.onTransitionEnd) &&
this.onTransitionEnd(loc => (isCanceled ? this.createBase(loc) === this.base() : false));
}
markAsObsolete() {
this.isObsolete = true;
}
getIsObsolete() {
return this.isObsolete;
}
setOnRestore(fn) {
this.onRestore = fn;
}
setCreateLoc(fn) {
this.createLoc = fn;
}
createBase(loc) {
const [base] = loc.split(HOOK_DELIMETER);
return base;
}
base() {
return this.createBase(this.loc());
}
loc() {
return this.createLoc();
}
setOnTransitionEnd(fn) {
this.onTransitionEnd = fn;
}
}
function collectFlags(task, tasks) {
const base = task.base();
let hasTopUpdate = false;
let hasHostUpdate = false;
let hasChildUpdate = false;
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
const $base = task.base();
if ($base.length < base.length && base.indexOf($base) === 0) {
hasTopUpdate = true;
} else if ($base === base) {
hasHostUpdate = true;
} else if ($base.length > base.length && $base.indexOf(base) === 0) {
hasChildUpdate = true;
}
}
return {
hasTopUpdate,
hasHostUpdate,
hasChildUpdate,
};
}
function detectHasExact(task, tasks) {
const $loc = task.loc();
const hasExact = tasks.some(x => x.loc() === $loc);
return hasExact;
}
function createTask(callback, options) {
const { priority = TaskPriority.NORMAL, forceAsync = false, isTransition = false, loc, onTransitionEnd } = options;
const task = new Task(callback, priority, forceAsync);
task.setIsTransition(isTransition);
task.setOnTransitionEnd(onTransitionEnd);
task.setCreateLoc(loc || rootLoc);
return task;
}
const rootLoc = () => '>';
const scheduler = new Scheduler();
export { scheduler };
//# sourceMappingURL=scheduler.js.map