savehandler.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ServiceManager
  5. } from '@jupyterlab/services';
  6. import {
  7. IDisposable
  8. } from 'phosphor/lib/core/disposable';
  9. import {
  10. clearSignalData
  11. } from 'phosphor/lib/core/signaling';
  12. import {
  13. okButton, cancelButton, showDialog
  14. } from '../common/dialog';
  15. import {
  16. DocumentRegistry
  17. } from '../docregistry';
  18. /**
  19. * A class that manages the auto saving of a document.
  20. *
  21. * #### Notes
  22. * Implements https://github.com/ipython/ipython/wiki/IPEP-15:-Autosaving-the-IPython-Notebook.
  23. */
  24. export
  25. class SaveHandler implements IDisposable {
  26. /**
  27. * Construct a new save handler.
  28. */
  29. constructor(options: SaveHandler.IOptions) {
  30. this._manager = options.manager;
  31. this._context = options.context;
  32. this._minInterval = options.saveInterval * 1000 || 120000;
  33. this._interval = this._minInterval;
  34. // Restart the timer when the contents model is updated.
  35. this._context.fileChanged.connect(this._setTimer, this);
  36. this._context.disposed.connect(this.dispose, this);
  37. }
  38. /**
  39. * The save interval used by the timer (in seconds).
  40. */
  41. get saveInterval(): number {
  42. return this._interval / 1000;
  43. }
  44. set saveInterval(value: number) {
  45. this._minInterval = this._interval = value * 1000;
  46. if (this._isActive) {
  47. this._setTimer();
  48. }
  49. }
  50. /**
  51. * Get whether the handler is active.
  52. */
  53. get isActive(): boolean {
  54. return this._isActive;
  55. }
  56. /**
  57. * Get whether the save handler is disposed.
  58. */
  59. get isDisposed(): boolean {
  60. return this._context === null;
  61. }
  62. /**
  63. * Dispose of the resources used by the save handler.
  64. */
  65. dispose(): void {
  66. if (this._context === null) {
  67. return;
  68. }
  69. this._context = null;
  70. clearTimeout(this._autosaveTimer);
  71. clearSignalData(this);
  72. }
  73. /**
  74. * Start the autosaver.
  75. */
  76. start(): void {
  77. this._isActive = true;
  78. this._setTimer();
  79. }
  80. /**
  81. * Stop the autosaver.
  82. */
  83. stop(): void {
  84. this._isActive = false;
  85. clearTimeout(this._autosaveTimer);
  86. }
  87. /**
  88. * Set the timer.
  89. */
  90. private _setTimer(): void {
  91. clearTimeout(this._autosaveTimer);
  92. if (!this._isActive) {
  93. return;
  94. }
  95. this._autosaveTimer = window.setTimeout(() => {
  96. this._save();
  97. }, this._interval);
  98. }
  99. /**
  100. * Handle an autosave timeout.
  101. */
  102. private _save(): void {
  103. let context = this._context;
  104. // Trigger the next update.
  105. this._setTimer();
  106. if (!context) {
  107. return;
  108. }
  109. // Bail if the model is not dirty or it is read only, or the dialog
  110. // is already showing.
  111. if (!context.model.dirty || context.model.readOnly || this._inDialog) {
  112. return;
  113. }
  114. // Make sure the file has not changed on disk.
  115. let promise = this._manager.contents.get(context.path);
  116. promise.then(model => {
  117. if (!this.isDisposed && context.contentsModel &&
  118. model.last_modified !== context.contentsModel.last_modified) {
  119. return this._timeConflict(model.last_modified);
  120. }
  121. return this._finishSave();
  122. }, (err) => {
  123. return this._finishSave();
  124. }).catch(err => {
  125. console.error('Error in Auto-Save', err.message);
  126. });
  127. }
  128. /**
  129. * Handle a time conflict.
  130. */
  131. private _timeConflict(modified: string): Promise<void> {
  132. let localTime = new Date(this._context.contentsModel.last_modified);
  133. let remoteTime = new Date(modified);
  134. console.warn(`Last saving peformed ${localTime} ` +
  135. `while the current file seem to have been saved ` +
  136. `${remoteTime}`);
  137. let body = `The file has changed on disk since the last time we ` +
  138. `opened or saved it. ` +
  139. `Do you want to overwrite the file on disk with the version ` +
  140. ` open here, or load the version on disk (revert)?`;
  141. this._inDialog = true;
  142. return showDialog({
  143. title: 'File Changed', body, okText: 'OVERWRITE',
  144. buttons: [cancelButton, { text: 'REVERT' }, okButton]
  145. }).then(result => {
  146. if (this.isDisposed) {
  147. return;
  148. }
  149. this._inDialog = false;
  150. if (result.text === 'OVERWRITE') {
  151. return this._finishSave();
  152. } else if (result.text === 'REVERT') {
  153. return this._context.revert();
  154. }
  155. });
  156. }
  157. /**
  158. * Perform the save, adjusting the save interval as necessary.
  159. */
  160. private _finishSave(): Promise<void> {
  161. let start = new Date().getTime();
  162. return this._context.save().then(() => {
  163. if (this.isDisposed) {
  164. return;
  165. }
  166. let duration = new Date().getTime() - start;
  167. // New save interval: higher of 10x save duration or min interval.
  168. this._interval = Math.max(10 * duration, this._minInterval);
  169. // Restart the update to pick up the new interval.
  170. this._setTimer();
  171. });
  172. }
  173. private _autosaveTimer = -1;
  174. private _minInterval = -1;
  175. private _interval = -1;
  176. private _context: DocumentRegistry.Context = null;
  177. private _manager: ServiceManager.IManager = null;
  178. private _isActive = false;
  179. private _inDialog = false;
  180. }
  181. /**
  182. * A namespace for `SaveHandler` statics.
  183. */
  184. export
  185. namespace SaveHandler {
  186. /**
  187. * The options used to create a save handler.
  188. */
  189. export
  190. interface IOptions {
  191. /**
  192. * The context asssociated with the file.
  193. */
  194. context: DocumentRegistry.Context;
  195. /**
  196. * The service manager to use for checking last saved.
  197. */
  198. manager: ServiceManager.IManager;
  199. /**
  200. * The minimum save interval in seconds (default is two minutes).
  201. */
  202. saveInterval?: number;
  203. }
  204. }