savehandler.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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 '../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.isDisposed) {
  67. return;
  68. }
  69. clearTimeout(this._autosaveTimer);
  70. this._context = null;
  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 = setInterval(() => {
  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 (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. this._inDialog = false;
  147. if (result.text === 'OVERWRITE') {
  148. return this._finishSave();
  149. } else if (result.text === 'REVERT') {
  150. return this._context.revert();
  151. }
  152. });
  153. }
  154. /**
  155. * Perform the save, adjusting the save interval as necessary.
  156. */
  157. private _finishSave(): Promise<void> {
  158. let start = new Date().getTime();
  159. return this._context.save().then(() => {
  160. let duration = new Date().getTime() - start;
  161. // New save interval: higher of 10x save duration or min interval.
  162. this._interval = Math.max(10 * duration, this._minInterval);
  163. // Restart the update to pick up the new interval.
  164. this._setTimer();
  165. });
  166. }
  167. private _autosaveTimer = -1;
  168. private _minInterval = -1;
  169. private _interval = -1;
  170. private _context: DocumentRegistry.IContext<DocumentRegistry.IModel> = null;
  171. private _manager: ServiceManager.IManager = null;
  172. private _isActive = false;
  173. private _inDialog = false;
  174. }
  175. /**
  176. * A namespace for `SaveHandler` statics.
  177. */
  178. export
  179. namespace SaveHandler {
  180. /**
  181. * The options used to create a save handler.
  182. */
  183. export
  184. interface IOptions {
  185. /**
  186. * The context asssociated with the file.
  187. */
  188. context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
  189. /**
  190. * The service manager to use for checking last saved.
  191. */
  192. manager: ServiceManager.IManager;
  193. /**
  194. * The minimum save interval in seconds (default is two minutes).
  195. */
  196. saveInterval?: number;
  197. }
  198. }