diff --git a/packages/doenetml-worker/src/Core.js b/packages/doenetml-worker/src/Core.js index 82dba01e0..7f4bd3a1e 100644 --- a/packages/doenetml-worker/src/Core.js +++ b/packages/doenetml-worker/src/Core.js @@ -229,6 +229,9 @@ export default class Core { this.errorWarnings.errors.push(...res.errors); this.errorWarnings.warnings.push(...res.warnings); + this.failedToSavePageState = false; + this.failedToSaveCreditForItem = false; + // console.log(`serialized components at the beginning`) // console.log(deepClone(serializedComponents)); @@ -455,7 +458,6 @@ export default class Core { this.saveSubmissions({ pageCreditAchieved: await this.document.stateValues.creditAchieved, - suppressToast: true, }); } @@ -10810,7 +10812,6 @@ export default class Core { canSkipUpdatingRenderer = false, skipRendererUpdate = false, sourceInformation = {}, - suppressToast = false, // temporary }) { if (warnings && warnings.length > 0) { this.errorWarnings.warnings.push(...warnings); @@ -10985,7 +10986,7 @@ export default class Core { // so don't record the submission to the attempt tables // (the event will still get recorded) if (this.itemNumber > 0) { - this.saveSubmissions({ pageCreditAchieved, suppressToast }); + this.saveSubmissions({ pageCreditAchieved }); } } @@ -11180,14 +11181,6 @@ export default class Core { // console.log(">>>>resp from record event", resp.data) } catch (e) { console.error(`Error saving event: ${e.message}`); - // postMessage({ - // messageType: "sendToast", - // coreId: this.coreId, - // args: { - // message: `Error saving event: ${e.message}`, - // alertType: "error" - // } - // }) } } @@ -12707,6 +12700,26 @@ export default class Core { } } + async saveImmediately() { + if (this.savePageStateTimeoutID) { + // if in debounce to save page to local storage + // then immediate save to local storage + // and override timeout to save to database + clearTimeout(this.savePageStateTimeoutID); + await this.saveState(true); + } else { + // else override timeout to save any pending changes to database + await this.saveChangesToDatabase(true); + } + + postMessage({ + messageType: "saveImmediatelyResult", + success: !( + this.failedToSavePageState || this.failedToSaveCreditForItem + ), + }); + } + async saveState(overrideThrottle = false) { this.savePageStateTimeoutID = null; @@ -12788,10 +12801,10 @@ export default class Core { }, 60000); // TODO: find out how to test if not online - // and send this toast if not online: + // and send this alert if not online: // postMessage({ - // messageType: "sendToast", + // messageType: "sendAlert", // coreId: this.coreId, // args: { // message: "You're not connected to the internet. Changes are not saved. ", @@ -12808,14 +12821,18 @@ export default class Core { ); } catch (e) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: "Error synchronizing data. Changes not saved to the server.", alertType: "error", + id: "dataError", }, }); + + this.failedToSavePageState = true; + return; } @@ -12823,13 +12840,17 @@ export default class Core { if (resp.status === null) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: `Error synchronizing data. Changes not saved to the server. Are you connected to the internet?`, alertType: "error", + id: "dataError", }, }); + + this.failedToSavePageState = true; + return; } @@ -12837,16 +12858,22 @@ export default class Core { if (!data.success) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: data.message, alertType: "error", + id: "dataError", }, }); + + this.failedToSavePageState = true; + return; } + this.failedToSavePageState = false; + this.serverSaveId = data.saveId; if (this.flags.allowLocalState) { @@ -12910,7 +12937,7 @@ export default class Core { // console.log(">>>>recordContentInteraction data",data) } - saveSubmissions({ pageCreditAchieved, suppressToast = false }) { + saveSubmissions({ pageCreditAchieved }) { if (!this.flags.allowSaveSubmissions) { return; } @@ -12922,7 +12949,7 @@ export default class Core { itemNumber: this.itemNumber, }; - console.log("payload for save credit for item", payload); + // console.log("payload for save credit for item", payload); axios .post(this.apiURLs.saveCreditForItem, payload) @@ -12931,22 +12958,28 @@ export default class Core { if (resp.status === null) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: `Credit not saved due to error. Are you connected to the internet?`, alertType: "error", + id: "creditDataError", }, }); + + this.failedToSaveCreditForItem = true; } else if (!resp.data.success) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: `Credit not saved due to error: ${resp.data.message}`, alertType: "error", + id: "creditDataError", }, }); + + this.failedToSaveCreditForItem = true; } else { let data = resp.data; @@ -12966,81 +12999,85 @@ export default class Core { }, }); + this.failedToSaveCreditForItem = false; + //TODO: need type warning (red but doesn't hang around) if (data.viewedSolution) { - if (!suppressToast) { - postMessage({ - messageType: "sendToast", - coreId: this.coreId, - args: { - message: - "No credit awarded since solution was viewed.", - alertType: "info", - }, - }); - } + postMessage({ + messageType: "sendAlert", + coreId: this.coreId, + args: { + message: + "No credit awarded since solution was viewed.", + alertType: "info", + id: "solutionViewed", + }, + }); } if (data.timeExpired) { - if (!suppressToast) { - postMessage({ - messageType: "sendToast", - coreId: this.coreId, - args: { - message: - "No credit awarded since the time allowed has expired.", - alertType: "info", - }, - }); - } + postMessage({ + messageType: "sendAlert", + coreId: this.coreId, + args: { + message: + "No credit awarded since the time allowed has expired.", + alertType: "info", + id: "timeExpired", + }, + }); } if (data.pastDueDate) { - if (!suppressToast) { - postMessage({ - messageType: "sendToast", - coreId: this.coreId, - args: { - message: - "No credit awarded since the due date has passed.", - alertType: "info", - }, - }); - } + postMessage({ + messageType: "sendAlert", + coreId: this.coreId, + args: { + message: + "No credit awarded since the due date has passed.", + alertType: "info", + id: "pastDue", + }, + }); } if (data.exceededAttemptsAllowed) { - if (!suppressToast) { - postMessage({ - messageType: "sendToast", - coreId: this.coreId, - args: { - message: - "No credit awarded since no more attempts are allowed.", - alertType: "info", - }, - }); - } + postMessage({ + messageType: "sendAlert", + coreId: this.coreId, + args: { + message: + "No credit awarded since no more attempts are allowed.", + alertType: "info", + id: "noMoreAttempts", + }, + }); } if (data.databaseError) { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: "Credit not saved due to database error.", alertType: "error", + id: "creditDataError", }, }); + + this.failedToSaveCreditForItem = true; } } }) .catch((e) => { postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message: `Credit not saved due to error: ${e.message}`, alertType: "error", + id: "creditDataError", }, }); + + this.failedToSaveCreditForItem = true; }); } @@ -13173,11 +13210,12 @@ export default class Core { if (resp.status === null) { let message = `Cannot show solution due to error. Are you connected to the internet?`; postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message, alertType: "error", + id: "solutionDataError", }, }); return { @@ -13206,11 +13244,12 @@ export default class Core { let message = `Cannot show solution due to error.`; postMessage({ - messageType: "sendToast", + messageType: "sendAlert", coreId: this.coreId, args: { message, alertType: "error", + id: "solutionDataError", }, }); @@ -13320,15 +13359,10 @@ export default class Core { } } - if (this.savePageStateTimeoutID) { - // if in debounce to save page to local storage - // then immediate save to local storage - // and override timeout to save to database - clearTimeout(this.savePageStateTimeoutID); - await this.saveState(true); - } else { - // else override timeout to save any pending changes to database - await this.saveChangesToDatabase(true); + await this.saveImmediately(); + + if (this.failedToSavePageState || this.failedToSaveCreditForItem) { + throw Error("Terminating core failed due to failure to save data."); } } diff --git a/packages/doenetml-worker/src/CoreWorker.js b/packages/doenetml-worker/src/CoreWorker.js index 08b1af2a1..51002fcce 100644 --- a/packages/doenetml-worker/src/CoreWorker.js +++ b/packages/doenetml-worker/src/CoreWorker.js @@ -97,10 +97,14 @@ globalThis.onmessage = function (e) { } } else if (e.data.messageType === "terminate") { if (core) { - core.terminate().then(() => { - core = null; - postMessage({ messageType: "terminated" }); - }); + core.terminate() + .then(() => { + core = null; + postMessage({ messageType: "terminated" }); + }) + .catch(() => { + postMessage({ messageType: "terminateFailed" }); + }); } else { postMessage({ messageType: "terminated" }); } @@ -110,6 +114,8 @@ globalThis.onmessage = function (e) { actionName: "submitAllAnswers", args: e.data.args, }); + } else if (e.data.messageType === "saveImmediately") { + core.saveImmediately(); } }; diff --git a/packages/doenetml-worker/src/components/Answer.js b/packages/doenetml-worker/src/components/Answer.js index 6f047d159..3f5a2496c 100644 --- a/packages/doenetml-worker/src/components/Answer.js +++ b/packages/doenetml-worker/src/components/Answer.js @@ -240,13 +240,6 @@ export default class Answer extends InlineComponent { public: true, }; - // temporary attribute until fix toast - attributes.suppressToast = { - createComponentOfType: "boolean", - createStateVariable: "suppressToast", - defaultValue: false, - }; - return attributes; } @@ -2228,7 +2221,6 @@ export default class Answer extends InlineComponent { creditAchieved, }, }, - suppressToast: await this.stateValues.suppressToast, // temporary }); return await this.coreFunctions.triggerChainedActions({ diff --git a/packages/doenetml-worker/src/components/Document.js b/packages/doenetml-worker/src/components/Document.js index 4756f43bd..953807215 100644 --- a/packages/doenetml-worker/src/components/Document.js +++ b/packages/doenetml-worker/src/components/Document.js @@ -781,24 +781,28 @@ export default class Document extends BaseComponent { }, }); - let numAnswers = await this.stateValues.answerDescendants; - for (let [ - ind, - answer, - ] of await this.stateValues.answerDescendants.entries()) { + let answersToSubmit = []; + + for (let answer of await this.stateValues.answerDescendants) { if (!(await answer.stateValues.justSubmitted)) { - await this.coreFunctions.performAction({ - componentName: answer.componentName, - actionName: "submitAnswer", - args: { - actionId, - sourceInformation, - skipRendererUpdate: - skipRendererUpdate || ind < numAnswers - 1, - }, - }); + answersToSubmit.push(answer); } } + + let numAnswers = answersToSubmit.length; + + for (let [ind, answer] of answersToSubmit.entries()) { + await this.coreFunctions.performAction({ + componentName: answer.componentName, + actionName: "submitAnswer", + args: { + actionId, + sourceInformation, + skipRendererUpdate: + skipRendererUpdate || ind < numAnswers - 1, + }, + }); + } } recordVisibilityChange({ isVisible }) { diff --git a/packages/doenetml-worker/src/utils/expandDoenetML.js b/packages/doenetml-worker/src/utils/expandDoenetML.js index 44791727a..84e3efa8e 100644 --- a/packages/doenetml-worker/src/utils/expandDoenetML.js +++ b/packages/doenetml-worker/src/utils/expandDoenetML.js @@ -534,6 +534,7 @@ function substituteAttributeDeprecations(serializedComponents) { video: { height: { removeInVersion: 0.7 } }, conditionalcontent: { maximumnumbertoshow: { removeInVersion: 0.7 } }, angle: { draggable: { removeInVersion: 0.7 } }, + answer: { supresstoast: { removeInVersion: 0.7 } }, }; for (let component of serializedComponents) { diff --git a/packages/doenetml/src/Viewer/ActivityViewer.jsx b/packages/doenetml/src/Viewer/ActivityViewer.jsx index 68ab662c9..ca4c2ce54 100644 --- a/packages/doenetml/src/Viewer/ActivityViewer.jsx +++ b/packages/doenetml/src/Viewer/ActivityViewer.jsx @@ -1100,7 +1100,7 @@ export function ActivityViewer({ async function submitAllAndFinishAssessment() { setProcessingSubmitAll(true); - let terminatePromises = []; + let submitAndSavePromises = []; for (let [ pageInd, @@ -1108,37 +1108,43 @@ export function ActivityViewer({ ] of pageCoreWorkersInfo.current.entries()) { if (pageInfo.pageCoreCreated[pageInd]) { let actionId = nanoid(); - let resolveTerminatePromise; + let resolveSubmitAndSavePromise; + let rejectSubmitAndSavePromise; let coreWorker = coreWorkerInfo.coreWorker; - terminatePromises.push( + submitAndSavePromises.push( new Promise((resolve, reject) => { - resolveTerminatePromise = resolve; + resolveSubmitAndSavePromise = resolve; + rejectSubmitAndSavePromise = reject; }), ); - let terminateListener = function (e) { + let submitAllAndSaveListener = function (e) { if ( e.data.messageType === "resolveAction" && e.data.args.actionId === actionId ) { - // posting terminate will make sure page state gets saved - // (as navigating to another URL will not initiate a state save) coreWorker.postMessage({ - messageType: "terminate", + messageType: "saveImmediately", }); - } else if (e.data.messageType === "terminated") { + } else if (e.data.messageType === "saveImmediatelyResult") { coreWorker.removeEventListener( "message", - terminateListener, + submitAllAndSaveListener, ); - // resolve promise - resolveTerminatePromise(); + if (e.data.success) { + resolveSubmitAndSavePromise(); + } else { + rejectSubmitAndSavePromise(); + } } }; - coreWorker.addEventListener("message", terminateListener); + coreWorker.addEventListener( + "message", + submitAllAndSaveListener, + ); coreWorker.postMessage({ messageType: "submitAllAnswers", @@ -1147,9 +1153,24 @@ export function ActivityViewer({ } } - await Promise.all(terminatePromises); + try { + await Promise.all(submitAndSavePromises); + + await terminateAllCores(); + + await saveState({ overrideThrottle: true }); + } catch (e) { + sendAlert( + `An error occurred. Assessment was not successfully submitted.`, + "error", + ); + + setFinishAssessmentMessageOpen(false); + setProcessingSubmitAll(false); - await saveState({ overrideThrottle: true }); + // return so don't set activity as completed + return; + } setActivityAsCompleted?.(); @@ -1161,6 +1182,54 @@ export function ActivityViewer({ }); } + async function terminateAllCores() { + let terminatePromises = []; + + for (let [ + pageInd, + coreWorkerInfo, + ] of pageCoreWorkersInfo.current.entries()) { + if (pageInfo.pageCoreCreated[pageInd]) { + let resolveTerminatePromise; + let rejectTerminatePromise; + let coreWorker = coreWorkerInfo.coreWorker; + + terminatePromises.push( + new Promise((resolve, reject) => { + resolveTerminatePromise = resolve; + rejectTerminatePromise = reject; + }), + ); + + let terminateListener = function (e) { + if (e.data.messageType === "terminated") { + coreWorker.removeEventListener( + "message", + terminateListener, + ); + + resolveTerminatePromise(); + } else if (e.data.messageType === "terminateFailed") { + coreWorker.removeEventListener( + "message", + terminateListener, + ); + + rejectTerminatePromise(); + } + }; + + coreWorker.addEventListener("message", terminateListener); + + coreWorker.postMessage({ + messageType: "terminate", + }); + } + } + + await Promise.all(terminatePromises); + } + function setPageErrorsAndWarningsCallback(errorsAndWarnings, pageInd) { errorsAndWarningsByPage.current[pageInd] = errorsAndWarnings;