js/domreplay/replay.js
import Logger from './logger';
import { setStateReplay, setStateReady } from './state'
import RegistrySingleton from './registry';
import { dispatchReplayUpdateEventListener } from './dispatcher'
/**
* Replay singleton takes care of replay.
* @access public
*/
class Replay {
static storageKey;
static instance;
constructor() {
this.storageKey = 'DOMREPLAY_REPLAY_EVENTS';
if (!this.instance) {
this.instance = this;
this.cancel = false;
}
return this.instance;
}
/**
* Get event object stored in local storage.
* @returns {Object}
*/
get events() {
return JSON.parse(window.localStorage.getItem(this.storageKey));
}
/**
* Stop replay execution.
*/
stop() {
this.cancel = true;
}
/**
* update replay storage
* @param {Object} object object to store.
*/
updateReplayStorage(object) {
window.localStorage.setItem(this.storageKey, JSON.stringify(object));
}
/**
* Gets the current event index.
* @returns {*|number}
*/
getCurrentEventIndex() {
return this.events.currentEventIndex;
}
getReplaySpeed() {
return this.events ? this.events.replaySpeed : 1;
}
/**
* Increment current event index.
*/
incrementCurrentEventIndedx() {
this.updateReplayStorage({
...this.events,
currentEventIndex: this.getCurrentEventIndex() + 1
})
}
/**
* Get event Item by Index.
* @param index
* @returns {Object}
*/
getItem(index) {
return this.events.events[index];
}
/**
* Gets number of total evnets
* @returns {Number}
*/
getTotalEvents() {
return this.events.count;
}
/**
* Clears the storage.
*/
clear() {
window.localStorage.removeItem(this.storageKey);
Logger.debug('Replay stoage cleared!');
}
/**
* Load events into storage.
* adds a currentEventIndex which will be used
* to keep track on which event we are currently playing.
* @param events
*/
load(events) {
this.updateReplayStorage({
...events,
replaySpeed: 1.0,
currentEventIndex: 0
})
}
/**
* Sets the replay speed
* @param divider - higher is faster, lower is slower.
*/
setReplaySpeed(divider) {
RegistrySingleton.setReplaySpeedForAllEventsInRegistry(divider);
this.updateReplayStorage({
...this.events,
replaySpeed: divider
});
}
/**
* Ready state check, checks wheter the document is ready.
* resolves when the document is ready.
* @return {Promise} resolves when document is ready.
*/
readyStateCheck() {
return new Promise(resolve => {
const time = setInterval(() => {
if (document.readyState !== 'complete') {
Logger.debug('Waiting for dom to complete loading after navigation');
return;
}
clearInterval(time);
resolve();
}, 500);
});
}
/**
* Returns a promise which executes an event.
* Checks for cancellation which occurs when this.stop() has been called.
* Finds the event to be executed in replay storage.
* Increment the current event index.
* Finds the correct class based on event type in Registry.
* Execute the replay function on the event class and wait for it to resolve.
* @returns {Promise<Object> | Promise<undefined>} returns a Promise which rejects to an error and resolves to undefined.
*/
playNextEvent() {
return new Promise(async (resolve, reject) => {
if (this.cancel) {
this.cancel = false;
Logger.debug('Replay promise chain cancelled.');
reject('Cancelled by user');
}
const eventIndex = this.getCurrentEventIndex();
const event = this.getItem(eventIndex);
if (window.location.href !== event.location) {
window.location.assign(event.location);
const urlWithoutAnchor = window.location.origin + window.location.pathname + '#';
if (event.location.includes(urlWithoutAnchor)) {
window.location.reload();
}
return reject('Navigation needed');
}
dispatchReplayUpdateEventListener({ number: eventIndex + 1, of: this.getTotalEvents()});
Logger.debug(`Replaying event number ${eventIndex}`);
this.incrementCurrentEventIndedx();
const eventClass = RegistrySingleton.getEvent(event.type);
await eventClass.replay(event)
.then(() => {
Logger.debug(`Done replaying event number ${eventIndex}`);
})
.catch(err => {
return reject(err);
});
return resolve();
});
}
/**
* Builds the promise chain for replaying events, so every event will be executed in
* correct order.
* @access private
*/
_buildReplayChain() {
const currentIndex = this.getCurrentEventIndex();
const totalEvents = this.getTotalEvents();
let promise = this.readyStateCheck();
for(let i = currentIndex; i < totalEvents; i++) {
promise = promise.then(() => this.playNextEvent());
}
promise.then(() => setStateReady(true));
Logger.debug(`Done building replay chain`);
}
/**
* Replay events.
* @returns {Promise<void>}
*/
async replay() {
await setStateReplay()
.then(() => {
this.setReplaySpeed(this.getReplaySpeed());
this._buildReplayChain();
});
}
}
export default new Replay();