timing-object
Version:
An implementation of the timing object specification.
300 lines (232 loc) • 11.9 kB
text/typescript
import { ITimingObject, ITimingObjectEventMap, ITimingProvider, ITimingStateVector } from '../interfaces';
import { TConnectionState, TErrorEventHandler, TEventHandler, TTimingObjectConstructorFactory, TTimingStateVectorUpdate } from '../types';
export const createTimingObjectConstructor: TTimingObjectConstructorFactory = (
calculateTimeoutDelay,
createIllegalValueError,
createInvalidStateError,
eventTargetConstructor,
filterTimingStateVectorUpdate,
performance,
setTimeout,
translateTimingStateVector
) => {
return class extends eventTargetConstructor<ITimingObjectEventMap> implements ITimingObject {
private _endPosition: number;
private _onchange: null | [TEventHandler<this>, TEventHandler<this>];
private _onerror: null | [TErrorEventHandler<this>, TErrorEventHandler<this>];
private _onreadystatechange: null | [TEventHandler<this>, TEventHandler<this>];
private _readyState: TConnectionState;
private _skew: number;
private _startPosition: number;
private _timeoutId: null | number;
private _timingProviderSource: null | ITimingProvider;
private _vector: ITimingStateVector;
constructor(timingProviderSource?: ITimingProvider);
constructor(vector?: TTimingStateVectorUpdate, startPosition?: number, endPosition?: number);
constructor(timingProviderSourceOrVector = {}, startPosition = Number.NEGATIVE_INFINITY, endPosition = Number.POSITIVE_INFINITY) {
super();
const { timingProviderSource, vector } =
(<ITimingProvider>timingProviderSourceOrVector).update === undefined
? { timingProviderSource: null, vector: <ITimingStateVector>timingProviderSourceOrVector }
: { timingProviderSource: <ITimingProvider>timingProviderSourceOrVector, vector: {} };
this._endPosition = timingProviderSource === null ? endPosition : timingProviderSource.endPosition;
this._onchange = null;
this._onerror = null;
this._onreadystatechange = null;
this._readyState = timingProviderSource === null ? 'open' : timingProviderSource.readyState;
this._skew = timingProviderSource === null ? 0 : timingProviderSource.skew;
this._startPosition = timingProviderSource === null ? startPosition : timingProviderSource.startPosition;
this._timingProviderSource = timingProviderSource;
this._timeoutId = null;
this._vector =
timingProviderSource === null
? {
acceleration: 0,
position: 0,
velocity: 0,
...filterTimingStateVectorUpdate(vector),
timestamp: performance.now() / 1000
}
: timingProviderSource.vector;
// @todo The spec doesn't require to check if the endPosition is actually greater than the startPosition.
if (endPosition < this._vector.position) {
this._vector = { ...this._vector, acceleration: 0, position: endPosition, velocity: 0 };
}
if (startPosition > this._vector.position) {
this._vector = { ...this._vector, acceleration: 0, position: startPosition, velocity: 0 };
}
/*
* @todo Check if the vector would leave the range immediately.
* @todo The specification requires to run this._setInternalTimeout() only if the vector had to be modified above but it
* probably should run in either case.
* https://webtiming.github.io/timingobject/#x5-1-create-a-new-timing-object
*/
this._setInternalTimeout();
if (timingProviderSource === null) {
setTimeout(() => this.dispatchEvent(new Event('readystatechange')));
} else {
const onAdjust = () => {
this._skew = timingProviderSource.skew;
/*
* @todo Process skew change with newSkew as parameter.
* https://webtiming.github.io/timingobject/#x5-7-process-skew-change
* https://webtiming.github.io/timingobject/#x5-10-calculate-skew-adjustment
*/
};
const onChange = () => this._setInternalVector(timingProviderSource.vector);
const onReadyStateChange = () => {
if (this._isAllowedTransition(timingProviderSource.readyState)) {
this._readyState = timingProviderSource.readyState;
} else {
this._readyState = 'closed';
timingProviderSource.removeEventListener('adjust', onAdjust);
timingProviderSource.removeEventListener('change', onChange);
timingProviderSource.removeEventListener('readystatechange', onReadyStateChange);
}
if (timingProviderSource.error !== null) {
setTimeout(() => this.dispatchEvent(new ErrorEvent('error', { error: timingProviderSource.error })));
}
setTimeout(() => this.dispatchEvent(new Event('readystatechange')));
};
timingProviderSource.addEventListener('adjust', onAdjust);
timingProviderSource.addEventListener('change', onChange);
timingProviderSource.addEventListener('readystatechange', onReadyStateChange);
}
}
get endPosition(): number {
return this._endPosition;
}
get onchange(): null | TEventHandler<this> {
return this._onchange === null ? this._onchange : this._onchange[0];
}
set onchange(value) {
if (this._onchange !== null) {
this.removeEventListener('change', this._onchange[1]);
}
if (typeof value === 'function') {
const boundListener = value.bind(this);
this.addEventListener('change', boundListener);
this._onchange = [value, boundListener];
} else {
this._onchange = null;
}
}
get onerror(): null | TErrorEventHandler<this> {
return this._onerror === null ? this._onerror : this._onerror[0];
}
set onerror(value) {
if (this._onerror !== null) {
this.removeEventListener('error', this._onerror[1]);
}
if (typeof value === 'function') {
const boundListener = value.bind(this);
this.addEventListener('error', boundListener);
this._onerror = [value, boundListener];
} else {
this._onerror = null;
}
}
get onreadystatechange(): null | TEventHandler<this> {
return this._onreadystatechange === null ? this._onreadystatechange : this._onreadystatechange[0];
}
set onreadystatechange(value) {
if (this._onreadystatechange !== null) {
this.removeEventListener('readystatechange', this._onreadystatechange[1]);
}
if (typeof value === 'function') {
const boundListener = value.bind(this);
this.addEventListener('readystatechange', boundListener);
this._onreadystatechange = [value, boundListener];
} else {
this._onreadystatechange = null;
}
}
get readyState(): TConnectionState {
return this._readyState;
}
get startPosition(): number {
return this._startPosition;
}
get timingProviderSource(): null | ITimingProvider {
return this._timingProviderSource;
}
public query(): ITimingStateVector {
if (this._readyState !== 'open') {
throw createInvalidStateError();
}
const timestamp = performance.now() / 1000;
// @todo Compute the delta by gradually applying the skew.
const delta =
this._timingProviderSource === null ? timestamp - this._vector.timestamp : timestamp + this._skew - this._vector.timestamp;
const vector = translateTimingStateVector(this._vector, delta);
if (this._endPosition < vector.position || this._startPosition > vector.position) {
this._setInternalVector({
...vector,
acceleration: 0,
position: this._endPosition < vector.position ? this._endPosition : this._startPosition,
velocity: 0
});
return this.query();
}
return vector;
}
public update(newVector: TTimingStateVectorUpdate): Promise<void> {
if (this._readyState !== 'open') {
return Promise.reject(createInvalidStateError());
}
if (this._timingProviderSource !== null) {
const promise = this._timingProviderSource.update(newVector);
if (promise instanceof Promise) {
return promise;
}
return Promise.reject(new TypeError('The timingProviderSource failed to return a promise.'));
}
const filteredVector = filterTimingStateVectorUpdate(newVector);
// Return immediately if there is nothing to update.
if (Object.keys(filteredVector).length === 0) {
return Promise.resolve();
}
const normalizedNewVector = { ...this.query(), ...filteredVector };
const { position, velocity, acceleration } = normalizedNewVector;
if (
position < this._startPosition ||
position > this._endPosition ||
(position === this._startPosition && (velocity < 0 || (velocity === 0 && acceleration < 0))) ||
(position === this._endPosition && (velocity > 0 || (velocity === 0 && acceleration > 0)))
) {
return Promise.reject(createIllegalValueError());
}
this._setInternalVector(normalizedNewVector);
return Promise.resolve();
}
private _isAllowedTransition(readyState: TConnectionState): boolean {
return (
(this._readyState === 'closing' && readyState === 'closed') ||
this._readyState === 'connecting' ||
(this._readyState === 'open' && (readyState === 'closed' || readyState === 'closing'))
);
}
private _setInternalTimeout(): void {
if (this._timeoutId !== null) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (
(this._endPosition === Number.POSITIVE_INFINITY && this._startPosition === Number.NEGATIVE_INFINITY) ||
(this._vector.acceleration === 0 && this._vector.velocity === 0)
) {
return;
}
const delay = calculateTimeoutDelay(this._vector, this._startPosition, this._endPosition);
if (delay === null) {
return;
}
this._timeoutId = setTimeout(() => this.query(), delay);
}
private _setInternalVector(vector: ITimingStateVector): void {
this._vector = vector;
this._setInternalTimeout();
setTimeout(() => this.dispatchEvent(new Event('change')));
}
};
};