diff --git a/.gitignore b/.gitignore index a547bf3..b747872 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? + +# Cypress run artifacts +cypress/screenshots/ +cypress/videos/ diff --git a/README.md b/README.md index 9da197b..e6583a4 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,37 @@ and the page decides how to display it: To consolidate, normalize the documents' `version` fields in the assignment source. Saved student state survives such a change: it is keyed on a source hash that deliberately ignores `version`. + +## Windowed mounting + +Every item's viewer component is always mounted, but memory tracks what the +student can see — the pagination window or viewport — instead of the +assignment's length. The embedded viewers register with a page-wide +_windowed mounting_ policy (`mountPolicy` from `@doenet/doenetml-iframe`): +items only boot their iframe (a multi-MB bundle parse plus a core worker) +when near the viewport or within the pagination window, simultaneous boots +are capped, and at most `maxLiveViewers` stay live — the rest are **parked**: +their state is flushed, their iframe is replaced by a placeholder, and they +are restored (student's typed work intact) when the student returns to them. +In paginated mode the current page and its neighbors are kept live, so page +flips within the window stay instant. + +This is the default. Tune it with the `mountPolicy` prop (overrides for +`maxLiveViewers`, `visibleMargin`, `parkDelayMs`, `flushTimeoutMs`, +`maxConcurrentBoots`): + +```tsx + +``` + +Parking requires a persistence path (`flags.allowSaveState` or +`flags.allowLocalState`) so no student work can be lost; without one, +viewers still mount lazily but stay live once booted. + +To cut the per-document core cost further, `useSharedCoreWorker` serves all +documents' cores from a shared worker pool instead of one dedicated ~100 MB +worker per document (default off; see `@doenet/doenetml-iframe`). diff --git a/package-lock.json b/package-lock.json index a26fc48..7dc258b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "seedrandom": "^3.0.5" }, "devDependencies": { + "@doenet/doenetml-iframe": "0.7.20-dev.332", "@eslint/js": "^9.39.1", "@types/object-hash": "^3.0.6", "@types/react": "^19.2.7", @@ -38,7 +39,7 @@ "vitest": "^4.0.15" }, "peerDependencies": { - "@doenet/doenetml-iframe": "^0.7.20-dev.326", + "@doenet/doenetml-iframe": "^0.7.20-dev.332", "react": "^19.2.3", "react-dom": "^19.2.3" } @@ -334,11 +335,11 @@ } }, "node_modules/@doenet/doenetml-iframe": { - "version": "0.7.20-dev.326", - "resolved": "https://registry.npmjs.org/@doenet/doenetml-iframe/-/doenetml-iframe-0.7.20-dev.326.tgz", - "integrity": "sha512-AVqNUorjP3Wo82Vh6NUZanecdIm62Bm8GA37jfguM7J+x/AAW8Y1oEPAkNFD34G5EISex1Sg/3XuZC1nzEiouQ==", + "version": "0.7.20-dev.332", + "resolved": "https://registry.npmjs.org/@doenet/doenetml-iframe/-/doenetml-iframe-0.7.20-dev.332.tgz", + "integrity": "sha512-V6/fa/ZOFfd3lFlaXabArMpMBlSv9xOfnHOEBfy3mRhizjwucriu2PxdV/GnynA/G6Y2Ayve3y7nK7PTGoaq/Q==", + "dev": true, "license": "AGPL-3.0-or-later", - "peer": true, "peerDependencies": { "better-react-mathjax": "^3.0.0", "react": "^19.2.3", @@ -1070,23 +1071,25 @@ } }, "node_modules/@mathjax/mathjax-newcm-font": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.1.2.tgz", - "integrity": "sha512-lZHMjNP2XbABHA3kVn40rbse5ERUeMEmrGH03qLkCwxq4/5Z/eNLr0akw1MmQcqTwCbvkx1BFcmJ7RCfbRlw3Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.1.3.tgz", + "integrity": "sha512-gzAB3dFHilHX1l5x2xUqRL+1jDQt3Fyza1DkEMVXWC4E8SvsGdlgEza47HYi2WhVcgfkvf4zgUGzuhbq3Pjlew==", + "dev": true, "license": "Apache-2.0", "peer": true }, "node_modules/@mathjax/src": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@mathjax/src/-/src-4.1.2.tgz", - "integrity": "sha512-7z3mQCQu4YqC1XyO4hMRkNTO49+5ZN8VtvBYKx+aGIXuGrlUvEAQZzeN/gf3tqZuVU4AI2yf1nYrrL1+BrSxIQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@mathjax/src/-/src-4.1.3.tgz", + "integrity": "sha512-rIrWquuBSoJuoMBdC/1qD+AUHTorlccPicoVy6P2xbUgnuDBpCcpbHtOAsB8L3hdCHtNBg92lF8e3Fz+pkcQbw==", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@mathjax/mathjax-newcm-font": "4.1.2", + "@mathjax/mathjax-newcm-font": "4.1.3", "mhchemparser": "^4.2.1", "mj-context-menu": "^1.0.0", - "speech-rule-engine": "5.0.0-rc.1" + "speech-rule-engine": "5.0.0-rc.4" } }, "node_modules/@microsoft/api-extractor": { @@ -2715,6 +2718,7 @@ "version": "0.9.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -3160,6 +3164,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/better-react-mathjax/-/better-react-mathjax-3.0.1.tgz", "integrity": "sha512-NfDraTcTkL5mFTLID7VI1apbfIMcgStL2ON9PFsViEZyzQikkOlRJFJumAkvntqt3G5BN/vxcP3W1H4n6b5oag==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -3577,6 +3582,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -4440,6 +4446,7 @@ "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -4953,6 +4960,7 @@ "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "dependencies": { @@ -4984,6 +4992,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -4991,9 +5000,10 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -5007,6 +5017,7 @@ "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "dependencies": { @@ -6393,6 +6404,7 @@ "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", "deprecated": "Version 4 replaces this package with the scoped package @mathjax/src", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -6406,6 +6418,7 @@ "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -6416,6 +6429,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "dev": true, "license": "Apache-2.0", "peer": true }, @@ -6423,6 +6437,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -6445,6 +6460,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "dev": true, "license": "Apache-2.0", "peer": true }, @@ -6521,6 +6537,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "engines": { @@ -6531,6 +6548,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-1.0.0.tgz", "integrity": "sha512-OSgBFQRCVhrZwRa9lhqnKcJU92dd/YXgBjup0uyeuj9bOaVsf4myOyrjU4PfhYkdDk6AqVUTov7v5uFMvIbduA==", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -6879,6 +6897,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true }, @@ -6933,6 +6952,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "dependencies": { @@ -6947,9 +6967,10 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "engines": { @@ -7413,6 +7434,7 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, "license": "BlueOak-1.0.0", "peer": true, "dependencies": { @@ -7837,9 +7859,10 @@ } }, "node_modules/speech-rule-engine": { - "version": "5.0.0-rc.1", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-5.0.0-rc.1.tgz", - "integrity": "sha512-WgxL1K0MXNXW7+4fn11B6rtJ+jZTnCQMkJ44yV3XmFDcpGBqFJrDEHouGuFryqB5hoG1ksE2zlss/m6KOWnbPA==", + "version": "5.0.0-rc.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-5.0.0-rc.4.tgz", + "integrity": "sha512-3dFcH2QtQNhtvUUc09TxoaEK4WG03B9Z57krFG9jocrKykKGu35BhIkWr0vgEAxZZomEWkeluff69y/EbYzP4Q==", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -8825,6 +8848,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "dev": true, "license": "MIT", "peer": true }, diff --git a/package.json b/package.json index b5fcc83..578e3f5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "prettier:check": "prettier --check ." }, "peerDependencies": { - "@doenet/doenetml-iframe": "^0.7.20-dev.326", + "@doenet/doenetml-iframe": "^0.7.20-dev.332", "react": "^19.2.3", "react-dom": "^19.2.3" }, @@ -49,6 +49,7 @@ "seedrandom": "^3.0.5" }, "devDependencies": { + "@doenet/doenetml-iframe": "0.7.20-dev.332", "@eslint/js": "^9.39.1", "@types/object-hash": "^3.0.6", "@types/react": "^19.2.7", diff --git a/src/Activity/Activity.tsx b/src/Activity/Activity.tsx index adb756a..d6697e8 100644 --- a/src/Activity/Activity.tsx +++ b/src/Activity/Activity.tsx @@ -1,4 +1,5 @@ import { memo } from "react"; +import type { MountPolicy } from "@doenet/doenetml-iframe"; import { DoenetMLFlags } from "../types"; import { ActivityState } from "./activityState"; import { SelectActivity } from "./SelectActivity"; @@ -13,24 +14,36 @@ export type ActivityCommonProps = { forceShowCorrectness?: boolean; forceShowSolution?: boolean; forceUnsuppressCheckwork?: boolean; + /** URL the `` renderer uses to link to other Doenet activities. */ doenetViewerUrl?: string; + /** URL used to resolve `` media references. */ + doenetMediaUrl?: string; + standaloneUrl?: string; + cssUrl?: string; + doenetmlVersion?: string; fetchExternalDoenetML?: (arg: string) => Promise; darkMode?: "dark" | "light"; showAnswerResponseMenu?: boolean; answerResponseCountsByItem?: Record[]; doenetStates: unknown[]; stateVersion: number; + /** The windowed mounting policy every item's viewer registers with. */ + mountPolicy: MountPolicy; + useSharedCoreWorker?: boolean; reportScoreAndStateCallback: (args: unknown) => void; - checkRender: (state: ActivityState) => boolean; checkHidden: (state: ActivityState) => boolean; + /** + * Whether an item's viewer should stay booted even while hidden or + * off-screen (the paginator marks the current page and its neighbors + * so page flips are instant). + */ + checkKeepLive: (state: ActivityState) => boolean; allowItemAttemptButtons?: boolean; generateNewItemAttempt?: ( id: string, initialQuestionCounter: number, ) => void; hasRenderedCallback: (id: string) => void; - reportVisibility?: boolean; - reportVisibilityCallback: (id: string, isVisible: boolean) => void; itemAttemptNumbers: number[]; itemIndexById: ReadonlyMap; itemWord: string; diff --git a/src/Activity/SelectActivity.tsx b/src/Activity/SelectActivity.tsx index 6d6f187..0a3d695 100644 --- a/src/Activity/SelectActivity.tsx +++ b/src/Activity/SelectActivity.tsx @@ -15,7 +15,7 @@ export const SelectActivity = memo(function SelectActivity({ } return ( -