MDL-76327 tiny_autosave: Use fetch + keepalive to reset autosave session

The autosave is reset on form submission, but if that form submission
happens at the same time as a page reload, the connection and/or server
is slow, then the connection may be aborted before the session is
removed.

This commit changes the autosave reset to use the fetch() API with a
keepalive flag.

Unfortunately we do not have a formal endpoint for this in Moodle JS so
this is a hackier approach than I would like. MDL-76463 has been opened
to investigate this.

This commit also fixes a situation where the autosave content is re-sent
when the user has typed in the editor and their next action is to click
on the submit button. This is now blocked for that editor instance.
This commit is contained in:
Andrew Nicols 2022-11-24 11:42:02 +08:00
parent 422da2ed45
commit 15eedeac4e
6 changed files with 37 additions and 9 deletions

View File

@ -1,4 +1,4 @@
define("tiny_autosave/options",["exports","./common","editor_tiny/options","editor_tiny/utils"],(function(_exports,_common,_options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getBackoffTime=void 0,Object.defineProperty(_exports,"getContextId",{enumerable:!0,get:function(){return _options.getContextId}}),Object.defineProperty(_exports,"getDraftItemId",{enumerable:!0,get:function(){return _options.getDraftItemId}}),_exports.register=_exports.markInitialised=_exports.isInitialised=_exports.getPageInstance=_exports.getPageHash=void 0;
define("tiny_autosave/options",["exports","./common","editor_tiny/options","editor_tiny/utils"],(function(_exports,_common,_options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getBackoffTime=void 0,Object.defineProperty(_exports,"getContextId",{enumerable:!0,get:function(){return _options.getContextId}}),Object.defineProperty(_exports,"getDraftItemId",{enumerable:!0,get:function(){return _options.getDraftItemId}}),_exports.setAutosaveHasReset=_exports.register=_exports.markInitialised=_exports.isInitialised=_exports.hasAutosaveHasReset=_exports.getPageInstance=_exports.getPageHash=void 0;
/**
* Options helper for the Moodle Tiny Autosave plugin.
*
@ -6,6 +6,6 @@ define("tiny_autosave/options",["exports","./common","editor_tiny/options","edit
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const initialisedOptionName=(0,_options.getPluginOptionName)(_common.pluginName,"initialised"),pageHashName=(0,_options.getPluginOptionName)(_common.pluginName,"pagehash"),pageInstanceName=(0,_options.getPluginOptionName)(_common.pluginName,"pageinstance"),backoffTime=(0,_options.getPluginOptionName)(_common.pluginName,"backoffTime");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(initialisedOptionName,{processor:"boolean",default:!1}),registerOption(pageHashName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(backoffTime,{processor:"number",default:500})};_exports.isInitialised=editor=>!!(0,_utils.ensureEditorIsValid)(editor)&&editor.options.get(initialisedOptionName);_exports.markInitialised=editor=>editor.options.set(initialisedOptionName,!0);_exports.getPageHash=editor=>editor.options.get(pageHashName);_exports.getPageInstance=editor=>editor.options.get(pageInstanceName);_exports.getBackoffTime=editor=>editor.options.get(backoffTime)}));
const initialisedOptionName=(0,_options.getPluginOptionName)(_common.pluginName,"initialised"),pageHashName=(0,_options.getPluginOptionName)(_common.pluginName,"pagehash"),pageInstanceName=(0,_options.getPluginOptionName)(_common.pluginName,"pageinstance"),backoffTime=(0,_options.getPluginOptionName)(_common.pluginName,"backoffTime"),autosaveHasReset=(0,_options.getPluginOptionName)(_common.pluginName,"autosaveHasReset");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(initialisedOptionName,{processor:"boolean",default:!1}),registerOption(pageHashName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(backoffTime,{processor:"number",default:500}),registerOption(autosaveHasReset,{processor:"boolean",default:!1})};_exports.isInitialised=editor=>!!(0,_utils.ensureEditorIsValid)(editor)&&editor.options.get(initialisedOptionName);_exports.markInitialised=editor=>editor.options.set(initialisedOptionName,!0);_exports.getPageHash=editor=>editor.options.get(pageHashName);_exports.getPageInstance=editor=>editor.options.get(pageInstanceName);_exports.getBackoffTime=editor=>editor.options.get(backoffTime);_exports.setAutosaveHasReset=editor=>editor.options.set(autosaveHasReset,!0);_exports.hasAutosaveHasReset=editor=>editor.options.get(autosaveHasReset)}));
//# sourceMappingURL=options.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {\n getContextId,\n getDraftItemId,\n getPluginOptionName,\n} from 'editor_tiny/options';\nimport {ensureEditorIsValid} from 'editor_tiny/utils';\n\nconst initialisedOptionName = getPluginOptionName(pluginName, 'initialised');\nconst pageHashName = getPluginOptionName(pluginName, 'pagehash');\nconst pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');\nconst backoffTime = getPluginOptionName(pluginName, 'backoffTime');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n registerOption(initialisedOptionName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(pageHashName, {\n processor: 'string',\n \"default\": '',\n });\n\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(backoffTime, {\n processor: 'number',\n \"default\": 500,\n });\n};\n\nexport const isInitialised = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return false;\n }\n\n return editor.options.get(initialisedOptionName);\n};\nexport const markInitialised = (editor) => editor.options.set(initialisedOptionName, true);\nexport const getPageHash = (editor) => editor.options.get(pageHashName);\nexport const getPageInstance = (editor) => editor.options.get(pageInstanceName);\nexport const getBackoffTime = (editor) => editor.options.get(backoffTime);\nexport {\n getContextId,\n getDraftItemId,\n};\n"],"names":["initialisedOptionName","pluginName","pageHashName","pageInstanceName","backoffTime","editor","registerOption","options","register","processor","get","set"],"mappings":";;;;;;;;MA+BMA,uBAAwB,gCAAoBC,mBAAY,eACxDC,cAAe,gCAAoBD,mBAAY,YAC/CE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,aAAc,gCAAoBH,mBAAY,iCAE3BI,eACfC,eAAiBD,OAAOE,QAAQC,SACtCF,eAAeN,sBAAuB,CAClCS,UAAW,mBACA,IAGfH,eAAeJ,aAAc,CACzBO,UAAW,iBACA,KAGfH,eAAeH,iBAAkB,CAC7BM,UAAW,iBACA,KAEfH,eAAeH,iBAAkB,CAC7BM,UAAW,iBACA,KAEfH,eAAeF,YAAa,CACxBK,UAAW,iBACA,8BAIWJ,WACrB,8BAAoBA,SAIlBA,OAAOE,QAAQG,IAAIV,gDAEEK,QAAWA,OAAOE,QAAQI,IAAIX,uBAAuB,wBACzDK,QAAWA,OAAOE,QAAQG,IAAIR,uCAC1BG,QAAWA,OAAOE,QAAQG,IAAIP,0CAC/BE,QAAWA,OAAOE,QAAQG,IAAIN"}
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {\n getContextId,\n getDraftItemId,\n getPluginOptionName,\n} from 'editor_tiny/options';\nimport {ensureEditorIsValid} from 'editor_tiny/utils';\n\nconst initialisedOptionName = getPluginOptionName(pluginName, 'initialised');\nconst pageHashName = getPluginOptionName(pluginName, 'pagehash');\nconst pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');\nconst backoffTime = getPluginOptionName(pluginName, 'backoffTime');\nconst autosaveHasReset = getPluginOptionName(pluginName, 'autosaveHasReset');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n registerOption(initialisedOptionName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(pageHashName, {\n processor: 'string',\n \"default\": '',\n });\n\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(backoffTime, {\n processor: 'number',\n \"default\": 500,\n });\n registerOption(autosaveHasReset, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const isInitialised = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return false;\n }\n\n return editor.options.get(initialisedOptionName);\n};\nexport const markInitialised = (editor) => editor.options.set(initialisedOptionName, true);\nexport const getPageHash = (editor) => editor.options.get(pageHashName);\nexport const getPageInstance = (editor) => editor.options.get(pageInstanceName);\nexport const getBackoffTime = (editor) => editor.options.get(backoffTime);\nexport const setAutosaveHasReset = (editor) => editor.options.set(autosaveHasReset, true);\nexport const hasAutosaveHasReset = (editor) => editor.options.get(autosaveHasReset);\n\nexport {\n getContextId,\n getDraftItemId,\n};\n"],"names":["initialisedOptionName","pluginName","pageHashName","pageInstanceName","backoffTime","autosaveHasReset","editor","registerOption","options","register","processor","get","set"],"mappings":";;;;;;;;MA+BMA,uBAAwB,gCAAoBC,mBAAY,eACxDC,cAAe,gCAAoBD,mBAAY,YAC/CE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,aAAc,gCAAoBH,mBAAY,eAC9CI,kBAAmB,gCAAoBJ,mBAAY,sCAEhCK,eACfC,eAAiBD,OAAOE,QAAQC,SACtCF,eAAeP,sBAAuB,CAClCU,UAAW,mBACA,IAGfH,eAAeL,aAAc,CACzBQ,UAAW,iBACA,KAGfH,eAAeJ,iBAAkB,CAC7BO,UAAW,iBACA,KAEfH,eAAeJ,iBAAkB,CAC7BO,UAAW,iBACA,KAEfH,eAAeH,YAAa,CACxBM,UAAW,iBACA,MAEfH,eAAeF,iBAAkB,CAC7BK,UAAW,mBACA,4BAIWJ,WACrB,8BAAoBA,SAIlBA,OAAOE,QAAQG,IAAIX,gDAEEM,QAAWA,OAAOE,QAAQI,IAAIZ,uBAAuB,wBACzDM,QAAWA,OAAOE,QAAQG,IAAIT,uCAC1BI,QAAWA,OAAOE,QAAQG,IAAIR,0CAC/BG,QAAWA,OAAOE,QAAQG,IAAIP,0CACzBE,QAAWA,OAAOE,QAAQI,IAAIP,kBAAkB,gCAChDC,QAAWA,OAAOE,QAAQG,IAAIN"}

View File

@ -1,10 +1,10 @@
define("tiny_autosave/repository",["exports","core/ajax","./options","core/pending","editor_tiny/utils"],(function(_exports,_ajax,Options,_pending,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateAutosaveSession=_exports.resumeAutosaveSession=_exports.removeAutosaveSession=void 0,Options=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
define("tiny_autosave/repository",["exports","core/ajax","core/config","./options","core/pending","editor_tiny/utils"],(function(_exports,_ajax,config,Options,_pending,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Repository helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/repository
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(Options),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const fetchOne=(methodname,args)=>(0,_ajax.call)([{methodname:methodname,args:args}])[0];_exports.resumeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:resumeAutosaveSession");return fetchOne("tiny_autosave_resume_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,draftid:Options.getDraftItemId(editor)}).then((result=>(pendingPromise.resolve(),result)))};_exports.updateAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:updateAutosaveSession");return fetchOne("tiny_autosave_update_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,drafttext:editor.getContent()}).then((result=>(pendingPromise.resolve(),result)))};_exports.removeAutosaveSession=editor=>(0,_utils.ensureEditorIsValid)(editor)?fetchOne("tiny_autosave_reset_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id}):Promise.reject("Invalid editor")}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateAutosaveSession=_exports.resumeAutosaveSession=_exports.removeAutosaveSession=void 0,config=_interopRequireWildcard(config),Options=_interopRequireWildcard(Options),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const fetchOne=(methodname,args)=>(0,_ajax.call)([{methodname:methodname,args:args}])[0];_exports.resumeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:resumeAutosaveSession");return fetchOne("tiny_autosave_resume_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,draftid:Options.getDraftItemId(editor)}).then((result=>(pendingPromise.resolve(),result)))};_exports.updateAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");if(Options.hasAutosaveHasReset(editor))return Promise.reject("Unable to store autosave content - content has been reset");const pendingPromise=new _pending.default("tiny_autosave/repository:updateAutosaveSession");return fetchOne("tiny_autosave_update_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,drafttext:editor.getContent()}).then((result=>(pendingPromise.resolve(),result)))};_exports.removeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))throw new Error("Invalid editor");Options.setAutosaveHasReset(editor);const requestUrl=new URL("".concat(config.wwwroot,"/lib/ajax/service.php"));requestUrl.searchParams.set("sesskey",config.sesskey);const args={contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id};fetch(requestUrl,{method:"POST",body:JSON.stringify([{index:0,methodname:"tiny_autosave_reset_session",args:args}]),keepalive:!0})}}));
//# sourceMappingURL=repository.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -33,6 +33,7 @@ const initialisedOptionName = getPluginOptionName(pluginName, 'initialised');
const pageHashName = getPluginOptionName(pluginName, 'pagehash');
const pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');
const backoffTime = getPluginOptionName(pluginName, 'backoffTime');
const autosaveHasReset = getPluginOptionName(pluginName, 'autosaveHasReset');
export const register = (editor) => {
const registerOption = editor.options.register;
@ -58,6 +59,10 @@ export const register = (editor) => {
processor: 'number',
"default": 500,
});
registerOption(autosaveHasReset, {
processor: 'boolean',
"default": false,
});
};
export const isInitialised = (editor) => {
@ -71,6 +76,9 @@ export const markInitialised = (editor) => editor.options.set(initialisedOptionN
export const getPageHash = (editor) => editor.options.get(pageHashName);
export const getPageInstance = (editor) => editor.options.get(pageInstanceName);
export const getBackoffTime = (editor) => editor.options.get(backoffTime);
export const setAutosaveHasReset = (editor) => editor.options.set(autosaveHasReset, true);
export const hasAutosaveHasReset = (editor) => editor.options.get(autosaveHasReset);
export {
getContextId,
getDraftItemId,

View File

@ -22,6 +22,7 @@
*/
import {call} from 'core/ajax';
import * as config from 'core/config';
import * as Options from './options';
import Pending from 'core/pending';
import {ensureEditorIsValid} from 'editor_tiny/utils';
@ -65,6 +66,9 @@ export const updateAutosaveSession = (editor) => {
if (!ensureEditorIsValid(editor)) {
return Promise.reject('Invalid editor');
}
if (Options.hasAutosaveHasReset(editor)) {
return Promise.reject('Unable to store autosave content - content has been reset');
}
const pendingPromise = new Pending('tiny_autosave/repository:updateAutosaveSession');
@ -85,17 +89,33 @@ export const updateAutosaveSession = (editor) => {
* Remove the Autosave session.
*
* @param {TinyMCE} editor The TinyMCE editor instance
* @returns {Promise<AutosaveSession>} The Autosave session
*/
export const removeAutosaveSession = (editor) => {
if (!ensureEditorIsValid(editor)) {
return Promise.reject('Invalid editor');
throw new Error('Invalid editor');
}
Options.setAutosaveHasReset(editor);
return fetchOne('tiny_autosave_reset_session', {
// Please note that we must use a Beacon send here.
// The XHR is not guaranteed because it will be aborted on page transition.
// https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
// Note: Moodle does not currently have a sendBeacon API endpoint.
const requestUrl = new URL(`${config.wwwroot}/lib/ajax/service.php`);
requestUrl.searchParams.set('sesskey', config.sesskey);
const args = {
contextid: Options.getContextId(editor),
pagehash: Options.getPageHash(editor),
pageinstance: Options.getPageInstance(editor),
elementid: editor.targetElm.id,
};
fetch(requestUrl, {
method: 'POST',
body: JSON.stringify([{
index: 0,
methodname: 'tiny_autosave_reset_session',
args,
}]),
keepalive: true,
});
};