Skip to content

Commit

Permalink
feat(scheduler): add shims and initializers
Browse files Browse the repository at this point in the history
  • Loading branch information
fkleuver committed Oct 9, 2019
1 parent 396cafe commit 341dd69
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 20 deletions.
112 changes: 111 additions & 1 deletion packages/runtime-html-browser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,117 @@
import { DI, IContainer, IRegistry, IResolver, Key, Registration } from '@aurelia/kernel';
import { IDOM, IDOMInitializer, ISinglePageApp } from '@aurelia/runtime';
import { IDOM, IDOMInitializer, ISinglePageApp, INativeSchedulers } from '@aurelia/runtime';
import { RuntimeHtmlConfiguration, HTMLDOM } from '@aurelia/runtime-html';

function createPromiseMicrotaskQueue() {
return (function () {
const p = Promise.resolve();
return function (cb: () => void) {
p.then(cb);
};
})();
}

function createMutationObserverMicrotaskQueue(window: Window) {
return (function () {
const queue: (() => void)[] = [];
function invokeOne(cb: () => void): void { cb(); }
function invokeAll(): void { queue.splice(0).forEach(invokeOne); }
const observer = new (window as unknown as { MutationObserver: typeof MutationObserver }).MutationObserver(invokeAll);
const node = window.document.createTextNode('a');
const flip = Object.assign(Object.create(null), { a: 'b', b: 'a' });

let val = 'a';
observer.observe(node, { characterData: true });

return function (cb: () => void) {
queue.push(cb);
node.data = val = flip[val];
};
})();
}

// Create a microtask queue, trying the available options starting with the most performant
const createMicrotaskQueue = (function () {
// Cache the created queue based on window instance to ensure we have at most one queue per window
const cache = new WeakMap<Window, (cb: () => void) => void>();

return function (wnd: Window) {
let microtaskQueue = cache.get(wnd);
if (microtaskQueue === void 0) {
if (Promise.toString().includes('[native code]')) {
microtaskQueue = createPromiseMicrotaskQueue();
} else {
microtaskQueue = createMutationObserverMicrotaskQueue(wnd);
}

cache.set(wnd, microtaskQueue);
}

return microtaskQueue;
};
})();

const createPostMessageQueue = (function () {
const cache = new WeakMap<Window, (cb: () => void) => void>();

return function (wnd: Window) {
let postMessageQueue = cache.get(wnd);
if (postMessageQueue === void 0) {
postMessageQueue = (function () {
const queue: (() => void)[] = [];
function invokeOne(cb: () => void): void { cb(); }
function invokeAll(): void { queue.splice(0).forEach(invokeOne); }

wnd.addEventListener('message', invokeAll);

return function (cb: () => void) {
queue.push(cb);
wnd.postMessage('', '*');
};
})();

cache.set(wnd, postMessageQueue);
}

return postMessageQueue;
};
})();

class BrowserSchedulers implements INativeSchedulers {
public readonly queueMicroTask: (cb: () => void) => void;
public readonly postMessage: (cb: () => void) => void;
public readonly setTimeout: (cb: () => void, timeout?: number) => number;
public readonly clearTimeout: (handle: number) => void;
public readonly setInterval: (cb: () => void, timeout?: number) => number;
public readonly clearInterval: (handle: number) => void;
public readonly requestAnimationFrame: (cb: () => void) => number;
public readonly cancelAnimationFrame: (handle: number) => void;
public readonly requestIdleCallback: (cb: () => void) => number;
public readonly cancelIdleCallback: (handle: number) => void;

public constructor(wnd: Window) {
if (wnd.queueMicrotask !== void 0 && wnd.queueMicrotask.toString().includes('[native code]')) {
this.queueMicroTask = wnd.queueMicrotask;
} else {
this.queueMicroTask = createMicrotaskQueue(wnd);
}
this.postMessage = createPostMessageQueue(wnd);
this.setTimeout = wnd.setTimeout;
this.clearTimeout = wnd.clearTimeout;
this.setInterval = wnd.setInterval;
this.clearInterval = wnd.clearInterval;
this.requestAnimationFrame = wnd.requestAnimationFrame;
this.cancelAnimationFrame = wnd.cancelAnimationFrame;
if (!('requestIdleCallback' in wnd)) {
this.requestIdleCallback = wnd.setTimeout;
this.cancelIdleCallback = wnd.clearTimeout;
} else {
this.requestIdleCallback = (wnd as unknown as { requestIdleCallback: INativeSchedulers['requestIdleCallback'] }).requestIdleCallback;
this.cancelIdleCallback = (wnd as unknown as { cancelIdleCallback: INativeSchedulers['cancelIdleCallback'] }).cancelIdleCallback;
}
}
}

class BrowserDOMInitializer implements IDOMInitializer {
public static readonly inject: readonly Key[] = [IContainer];

Expand Down
101 changes: 99 additions & 2 deletions packages/runtime-html-jsdom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,105 @@
import { DI, IContainer, IRegistry, IResolver, Key, Registration } from '@aurelia/kernel';
import { IDOM, IDOMInitializer, ISinglePageApp } from '@aurelia/runtime';
import { DI, IContainer, IRegistry, IResolver, Key, Registration, PLATFORM } from '@aurelia/kernel';
import { IDOM, IDOMInitializer, ISinglePageApp, INativeSchedulers } from '@aurelia/runtime';
import { RuntimeHtmlConfiguration, HTMLDOM } from '@aurelia/runtime-html';
import { JSDOM } from 'jsdom';

declare const process: NodeJS.Process;

function createNextTickMicrotaskQueue() {
return function (cb: () => void) {
process.nextTick(cb);
};
}

function createPromiseMicrotaskQueue() {
return (function () {
const p = Promise.resolve();
return function (cb: () => void) {
p.then(cb);
};
})();
}

// Create a microtask queue, trying the available options starting with the most performant
const createMicrotaskQueue = (function () {
// Cache the created queue based on window instance to ensure we have at most one queue per window
const cache = new WeakMap<Window, (cb: () => void) => void>();

return function (wnd: Window) {
let microtaskQueue = cache.get(wnd);
if (microtaskQueue === void 0) {
if (PLATFORM.isNodeLike && typeof process.nextTick === 'function') {
microtaskQueue = createNextTickMicrotaskQueue();
}
microtaskQueue = createPromiseMicrotaskQueue();

cache.set(wnd, microtaskQueue);
}

return microtaskQueue;
};
})();

const createPostMessageQueue = (function () {
const cache = new WeakMap<Window, (cb: () => void) => void>();

return function (wnd: Window) {
let postMessageQueue = cache.get(wnd);
if (postMessageQueue === void 0) {
postMessageQueue = (function () {
const queue: (() => void)[] = [];
function invokeOne(cb: () => void): void { cb(); }
function invokeAll(): void { queue.splice(0).forEach(invokeOne); }

wnd.addEventListener('message', invokeAll);

return function (cb: () => void) {
queue.push(cb);
wnd.postMessage('', '*');
};
})();

cache.set(wnd, postMessageQueue);
}

return postMessageQueue;
};
})();

class JSDOMSchedulers implements INativeSchedulers {
public readonly queueMicroTask: (cb: () => void) => void;
public readonly postMessage: (cb: () => void) => void;
public readonly setTimeout: (cb: () => void, timeout?: number) => number;
public readonly clearTimeout: (handle: number) => void;
public readonly setInterval: (cb: () => void, timeout?: number) => number;
public readonly clearInterval: (handle: number) => void;
public readonly requestAnimationFrame: (cb: () => void) => number;
public readonly cancelAnimationFrame: (handle: number) => void;
public readonly requestIdleCallback: (cb: () => void) => number;
public readonly cancelIdleCallback: (handle: number) => void;

public constructor(
private readonly jsdom: JSDOM,
) {
const wnd = jsdom.window;
this.queueMicroTask = createMicrotaskQueue(wnd);
this.postMessage = createPostMessageQueue(wnd);
this.setTimeout = wnd.setTimeout;
this.clearTimeout = wnd.clearTimeout;
this.setInterval = wnd.setInterval;
this.clearInterval = wnd.clearInterval;
this.requestAnimationFrame = wnd.requestAnimationFrame;
this.cancelAnimationFrame = wnd.cancelAnimationFrame;
if (!('requestIdleCallback' in wnd)) {
this.requestIdleCallback = wnd.setTimeout;
this.cancelIdleCallback = wnd.clearTimeout;
} else {
this.requestIdleCallback = (wnd as unknown as { requestIdleCallback: INativeSchedulers['requestIdleCallback'] }).requestIdleCallback;
this.cancelIdleCallback = (wnd as unknown as { cancelIdleCallback: INativeSchedulers['cancelIdleCallback'] }).cancelIdleCallback;
}
}
}

class JSDOMInitializer implements IDOMInitializer {
public static readonly inject: readonly Key[] = [IContainer];

Expand Down
14 changes: 9 additions & 5 deletions packages/runtime/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ import { PriorityBindingBehavior } from './resources/binding-behaviors/priority'
import { SignalBindingBehavior } from './resources/binding-behaviors/signals';
import { ThrottleBindingBehavior } from './resources/binding-behaviors/throttle';
import { FrequentMutations, InfrequentMutations, ObserveShallow } from './resources/custom-attributes/flags';
import {
Else,
If
} from './resources/custom-attributes/if';
import { Else, If } from './resources/custom-attributes/if';
import { Repeat } from './resources/custom-attributes/repeat';
import { Replaceable } from './resources/custom-attributes/replaceable';
import { With } from './resources/custom-attributes/with';
import { SanitizeValueConverter } from './resources/value-converters/sanitize';
import { ViewValueConverter } from './resources/value-converters/view';
import { ViewLocator } from './templating/view';
import { Clock, Scheduler } from './scheduler';

export const IObserverLocatorRegistration = ObserverLocator as IRegistry;
export const ILifecycleRegistration = Lifecycle as IRegistry;
export const IRendererRegistration = Renderer as IRegistry;
export const IStartTaskManagerRegistration = StartTaskManager as IRegistry;
export const IViewLocatorRegistration = ViewLocator as IRegistry;
export const IClockRegistration = Clock as IRegistry;
export const ISchedulerRegistration = Scheduler as IRegistry;

/**
* Default implementations for the following interfaces:
Expand All @@ -54,13 +54,17 @@ export const IViewLocatorRegistration = ViewLocator as IRegistry;
* - `IRenderer`
* - `IStartTaskManager`
* - `IViewLocator`
* - `IClockRegistration`
* - `ISchedulerRegistration`
*/
export const DefaultComponents = [
IObserverLocatorRegistration,
ILifecycleRegistration,
IRendererRegistration,
IStartTaskManagerRegistration,
IViewLocatorRegistration
IViewLocatorRegistration,
IClockRegistration,
ISchedulerRegistration,
];

export const FrequentMutationsRegistration = FrequentMutations as unknown as IRegistry;
Expand Down
18 changes: 18 additions & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,24 @@ export {
ViewValueConverter
} from './resources/value-converters/view';

export {
Clock,
IClock,
IClockSettings,
INativeSchedulers,
IScheduler,
ITask,
ITaskQueue,
QueueTaskOptions,
Scheduler,
Task,
TaskAbortError,
TaskCallback,
TaskQueue,
TaskQueuePriority,
TaskStatus,
} from './scheduler';

export {
bindable,
BindableDecorator,
Expand Down
30 changes: 18 additions & 12 deletions packages/runtime/src/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IDisposable, PLATFORM, DI } from '@aurelia/kernel';
import { IDisposable, PLATFORM, DI, IContainer, IResolver, Registration } from '@aurelia/kernel';

export const INativeSchedulers = DI.createInterface<INativeSchedulers>('INativeSchedulers').noDefault();
export interface INativeSchedulers {
Expand All @@ -16,10 +16,10 @@ export interface INativeSchedulers {

export interface IClockSettings {
refreshInterval?: number;
forceUpdateInterval?: number
forceUpdateInterval?: number;
now?(): number;
setInterval?(cb: () => void, timeout?: number): number,
clearInterval?(handle: number): void,
setInterval?(cb: () => void, timeout?: number): number;
clearInterval?(handle: number): void;
}

const defaultClockSettings: Required<IClockSettings> = {
Expand Down Expand Up @@ -57,6 +57,10 @@ export class Clock implements IClock {
clearInterval(handle);
};
}

public static register(container: IContainer): IResolver<IClock> {
return Registration.singleton(IClock, this).register(container);
}
}

export const globalClock = new Clock();
Expand All @@ -82,7 +86,7 @@ export interface IScheduler {
/* @internal */requestFlush(taskQueue: ITaskQueue): void;
}

export class Scheduler {
export class Scheduler implements IScheduler {
private readonly [TaskQueuePriority.microTask]: TaskQueue[];
private readonly [TaskQueuePriority.eventLoop]: TaskQueue[];
private readonly [TaskQueuePriority.render]: TaskQueue[];
Expand Down Expand Up @@ -121,6 +125,10 @@ export class Scheduler {
};
}

public static register(container: IContainer): IResolver<IScheduler> {
return Registration.singleton(IScheduler, this).register(container);
}

public getTaskQueue(priority: TaskQueuePriority): TaskQueue {
return this[priority][0];
}
Expand All @@ -136,7 +144,7 @@ export class Scheduler {

public requestFlush(taskQueue: TaskQueue): void {
const flush = () => {
this.flush(taskQueue.priority)
this.flush(taskQueue.priority);
};

switch (taskQueue.priority) {
Expand Down Expand Up @@ -212,7 +220,7 @@ type TaskQueueOptions = {
clock: IClock;
priority: TaskQueuePriority;
scheduler: IScheduler;
}
};

export type QueueTaskOptions = {
/**
Expand All @@ -228,7 +236,7 @@ export type QueueTaskOptions = {
*/
preempt?: boolean;
priority?: TaskQueuePriority;
}
};

const defaultQueueTaskOptions: Required<QueueTaskOptions> = {
delay: 0,
Expand Down Expand Up @@ -541,7 +549,7 @@ export class TaskQueue {
}

export class TaskAbortError<T = any> extends Error {
constructor(public task: Task<T>) {
public constructor(public task: Task<T>) {
super('Task was canceled.');
}
}
Expand Down Expand Up @@ -576,7 +584,7 @@ export class Task<T = any> implements ITask {
return promise;
}
case 'running':
throw new Error('Trying to await task from within task will cause a deadlock.')
throw new Error('Trying to await task from within task will cause a deadlock.');
case 'completed':
return this._result = Promise.resolve() as unknown as Promise<T>;
case 'canceled':
Expand Down Expand Up @@ -651,5 +659,3 @@ export class Task<T = any> implements ITask {
};
}
}


0 comments on commit 341dd69

Please sign in to comment.