@jupyterlab/docmanager
Version:
JupyterLab - Document Manager
182 lines (163 loc) • 4.45 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { IDisposable } from '@lumino/disposable';
import { Signal } from '@lumino/signaling';
/**
* A class that manages the auto saving of a document.
*
* #### Notes
* Implements https://github.com/ipython/ipython/wiki/IPEP-15:-Autosaving-the-IPython-Notebook.
*/
export class SaveHandler implements IDisposable {
/**
* Construct a new save handler.
*/
constructor(options: SaveHandler.IOptions) {
this._context = options.context;
this._isConnectedCallback = options.isConnectedCallback || (() => true);
const interval = options.saveInterval || 120;
this._minInterval = interval * 1000;
this._interval = this._minInterval;
// Restart the timer when the contents model is updated.
this._context.fileChanged.connect(this._setTimer, this);
this._context.disposed.connect(this.dispose, this);
}
/**
* The save interval used by the timer (in seconds).
*/
get saveInterval(): number {
return this._interval / 1000;
}
set saveInterval(value: number) {
this._minInterval = this._interval = value * 1000;
if (this._isActive) {
this._setTimer();
}
}
/**
* Get whether the handler is active.
*/
get isActive(): boolean {
return this._isActive;
}
/**
* Get whether the save handler is disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose of the resources used by the save handler.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
clearTimeout(this._autosaveTimer);
Signal.clearData(this);
}
/**
* Start the autosaver.
*/
start(): void {
this._isActive = true;
this._setTimer();
}
/**
* Stop the autosaver.
*/
stop(): void {
this._isActive = false;
clearTimeout(this._autosaveTimer);
}
/**
* Set the timer.
*/
private _setTimer(): void {
clearTimeout(this._autosaveTimer);
if (!this._isActive) {
return;
}
this._autosaveTimer = window.setTimeout(() => {
if (this._isConnectedCallback()) {
this._save();
}
}, this._interval);
}
/**
* Handle an autosave timeout.
*/
private _save(): void {
const context = this._context;
// Trigger the next update.
this._setTimer();
if (!context) {
return;
}
// Bail if the model is not dirty or the file is not writable, or the dialog
// is already showing.
const writable = context.contentsModel && context.contentsModel.writable;
if (!writable || !context.model.dirty || this._inDialog) {
return;
}
const start = new Date().getTime();
context
.save()
.then(() => {
if (this.isDisposed) {
return;
}
const duration = new Date().getTime() - start;
// New save interval: higher of 10x save duration or min interval.
this._interval = Math.max(
this._multiplier * duration,
this._minInterval
);
// Restart the update to pick up the new interval.
this._setTimer();
})
.catch(err => {
// If the user canceled the save, do nothing.
const { name } = err;
if (name === 'ModalCancelError' || name === 'ModalDuplicateError') {
return;
}
// Otherwise, log the error.
console.error('Error in Auto-Save', err.message);
});
}
private _autosaveTimer = -1;
private _minInterval = -1;
private _interval = -1;
private _context: DocumentRegistry.Context;
private _isConnectedCallback: () => boolean;
private _isActive = false;
private _inDialog = false;
private _isDisposed = false;
private _multiplier = 10;
}
/**
* A namespace for `SaveHandler` statics.
*/
export namespace SaveHandler {
/**
* The options used to create a save handler.
*/
export interface IOptions {
/**
* The context associated with the file.
*/
context: DocumentRegistry.Context;
/**
* Autosaving should be paused while this callback function returns `false`.
* By default, it always returns `true`.
*/
isConnectedCallback?: () => boolean;
/**
* The minimum save interval in seconds (default is two minutes).
*/
saveInterval?: number;
}
}