wed
Version:
Wed is a schema-aware editor for XML documents.
274 lines • 11.4 kB
JavaScript
/**
* Base class for savers.
* @author Louis-Dominique Dubeau
* @license MPL 2.0
* @copyright Mangalam Research Center for Buddhist Languages
*/
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
define(["require", "exports", "rxjs", "./browsers", "./serializer"], function (require, exports, rxjs_1, browsers, serializer) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
browsers = __importStar(browsers);
serializer = __importStar(serializer);
var SaveKind;
(function (SaveKind) {
SaveKind[SaveKind["AUTO"] = 1] = "AUTO";
SaveKind[SaveKind["MANUAL"] = 2] = "MANUAL";
})(SaveKind = exports.SaveKind || (exports.SaveKind = {}));
function deltaToString(delta) {
delta = Math.round(delta / 1000);
let timeDesc = "moments ago";
if (delta > 0) {
timeDesc = " ≈ ";
// To get a single digit after the decimal point, we divide by (factor /
// 10), round the result, and then divide by 10. Note that this is imprecise
// due to rounding errors in floating point arithmetic but we don't care.
if (delta > 60 * 60 * 24) {
timeDesc += `${Math.round(delta / (6 * 60 * 24)) / 10}d`;
}
else if (delta > 60 * 60) {
timeDesc += `${Math.round(delta / (6 * 60)) / 10}h`;
}
else if (delta > 60) {
timeDesc += `${Math.round(delta / 6) / 10}m`;
}
else {
timeDesc += `${delta}s`;
}
timeDesc += " ago";
}
return timeDesc;
}
/**
* A saver is responsible for saving a document's data. This class cannot be
* instantiated as-is, but only through subclasses.
*/
class Saver {
/**
* @param runtime The runtime under which this saver is created.
*
* @param version The version of wed for which this object is created.
*
* @param dataUpdater The updater that the editor created for its data tree.
*
* @param {Node} dataTree The editor's data tree.
*/
constructor(runtime, version, dataUpdater, dataTree, options) {
this.runtime = runtime;
this.version = version;
this.dataUpdater = dataUpdater;
this.dataTree = dataTree;
this.options = options;
/**
* Subclasses must set this variable to true once they have finished with
* their initialization.
*/
this.initialized = false;
/**
* Subclasses must set this variable to true if the saver is in a failed
* state. Note that the "failed" state is for cases where it makes no sense to
* attempt a recovery operation.
*
* One effect of being in a "failed" state is that the saver won't perform a
* recover operation if it is in a "failed" state.
*/
this.failed = false;
/**
* The generation that is currently being edited. It is mutable. Derived
* classes can read it but not modify it.
*/
this.currentGeneration = 0;
/**
* The generation that has last been saved. Derived classes can read it but
* not modify it.
*/
this.savedGeneration = 0;
/**
* The interval at which to autosave, in milliseconds.
*/
this.autosaveInterval = 0;
dataUpdater.events.subscribe((ev) => {
if (ev.name !== "Changed") {
return;
}
this.lastModification = Date.now();
if (this.savedGeneration === this.currentGeneration) {
this.currentGeneration++;
this._events.next({ name: "Changed" });
}
});
/**
* The _autosave method, pre-bound to ``this``.
* @private
*/
this._boundAutosave = this._autosave.bind(this);
this._events = new rxjs_1.Subject();
this.events = this._events.asObservable();
if (options.autosave !== undefined) {
this.setAutosaveInterval(options.autosave * 1000);
}
}
/**
* This method must be called when the user manually initiates a save.
*
* @returns A promise which resolves if the save was successful.
*/
save() {
return this._save(false);
}
/**
* This method returns the data to be saved in a save operation. Derived
* classes **must** call this method rather than get the data directly from
* the data tree.
*/
getData() {
const child = this.dataTree.firstChild;
if (browsers.MSIE) {
return serializer.serialize(child);
}
const serialization = child.outerHTML;
// Edge has the bad habit of adding a space before the forward slash in
// self-closing tags. Remove it.
return browsers.EDGE ? serialization.replace(/<([^/<>]+) \/>/g, "<$1/>") :
serialization;
}
/**
* Must be called by derived class upon a successful save.
*
* @param autosave ``true`` if called for an autosave operation, ``false`` if
* not.
*
* @param savingGeneration The generation being saved. It is necessary to pass
* this value due to the asynchronous nature of some saving operations.
*/
_saveSuccess(autosave, savingGeneration) {
// If we get here, we've been successful.
this.savedGeneration = savingGeneration;
this.lastSave = Date.now();
this.lastSaveKind = autosave ? SaveKind.AUTO : SaveKind.MANUAL;
this._events.next(autosave ? { name: "Autosaved" } : { name: "Saved" });
// This resets the countdown to now.
this.setAutosaveInterval(this.autosaveInterval);
}
/**
* Must be called by derived classes when they fail to perform their task.
*
* @param The error message associated with the failure. If the error message
* is specified a ``failed`` event will be emitted. If not, no event is
* emitted.
*/
_fail(error) {
this.failed = true;
if (error !== undefined) {
this._events.next({ name: "Failed", error });
}
}
/**
* This is the function called internally when an autosave is needed.
*/
_autosave() {
this.autosaveTimeout = undefined;
const done = () => {
// Calling ``setAutosaveInterval`` effectively starts a new timeout, and
// takes care of possible race conditions. For instance, a call to
// ``setAutosaveInterval`` could happen after the current timeout has
// started saving but before ``done`` is called. This would launch a new
// timeout. If the code here called ``setTimeout`` instead of
// ``setAutosaveInterval`` then two timeouts would be running.
this.setAutosaveInterval(this.autosaveInterval);
};
if (this.currentGeneration !== this.savedGeneration) {
// We have something to save!
// tslint:disable-next-line:no-floating-promises
this._save(true).then(done);
}
else {
done();
}
}
/**
* Changes the interval at which autosaves are performed. Note that calling
* this function will stop the current countdown and restart it from zero. If,
* for instance, the previous interval was 5 minutes, and 4 minutes had
* elapsed since the last save, the next autosave should happen one minute
* from now. However, if I now call this function with a new interval of 4
* minutes, this will cause the next autosave to happen 4 minutes after the
* call, rather than one minute.
*
* @param interval The interval between autosaves in milliseconds. 0 turns off
* autosaves.
*/
setAutosaveInterval(interval) {
this.autosaveInterval = interval;
const oldTimeout = this.autosaveTimeout;
if (oldTimeout !== undefined) {
clearTimeout(oldTimeout);
}
this.autosaveTimeout = interval !== 0 ?
setTimeout(this._boundAutosave, interval) : undefined;
}
/**
* This method is to be used by wed upon encountering a fatal error. It will
* attempt to record the last state of the data tree before wed dies.
*
* @returns A promise which resolves to ``undefined`` if the method did not do
* anything because the Saver object is in an unintialized state or has
* already failed. It resolves to ``true`` if the recovery operation was
* successful, and ``false`` if not.
*/
recover() {
return Promise.resolve().then(() => {
if (!this.initialized || this.failed) {
return Promise.resolve(undefined);
}
return this._recover();
});
}
/**
* Returns information regarding whether the saver sees the data tree as
* having been modified since the last save occurred.
*
* @returns ``false`` if the tree has not been modified. Otherwise, returns a
* string that describes how long ago the modification happened.
*/
getModifiedWhen() {
if (this.savedGeneration === this.currentGeneration ||
this.lastModification === undefined) {
return false;
}
return deltaToString(Date.now() - this.lastModification);
}
/**
* Produces a string that indicates in human readable format when the last
* save occurred.
*
* @returns The string. The value ``undefined`` is returned if no save has
* occurred yet.
*/
getSavedWhen() {
if (this.lastSave === undefined) {
return undefined;
}
return deltaToString(Date.now() - this.lastSave);
}
/**
* Returns the last kind of save that occurred.
*
* @returns {number|undefined} The kind. The value will be
* ``undefined`` if there has not been any save yet.
*/
getLastSaveKind() {
return this.lastSaveKind;
}
}
exports.Saver = Saver;
});
// LocalWords: param unintialized Mangalam MPL Dubeau autosaved autosaves pre
// LocalWords: autosave runtime autosaving setAutosaveInterval setTimeout
//# sourceMappingURL=saver.js.map