diff --git a/package-lock.json b/package-lock.json index a10419f7..c9315493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "mathjs": "^11.9.0", "merge-audio-buffers": "^1.0.0", "papaparse": "^5.4.1", + "retry": "^0.13.1", "stats.js": "^0.17.0", "sweetalert2": "^11.14.0", "virtual-keypad": "^5.15.2", @@ -113,6 +114,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -849,6 +851,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.13.tgz", "integrity": "sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", @@ -906,6 +909,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.43.tgz", "integrity": "sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.10.13", "@firebase/component": "0.6.9", @@ -918,7 +922,8 @@ "version": "0.9.2", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.7.9", @@ -1339,6 +1344,7 @@ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -2771,7 +2777,8 @@ "node_modules/@types/node": { "version": "18.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz", - "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==" + "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==", + "peer": true }, "node_modules/@types/pako": { "version": "1.0.4", @@ -2951,6 +2958,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.10.0", "@typescript-eslint/types": "6.10.0", @@ -3410,6 +3418,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3452,6 +3461,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4030,6 +4040,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5602,6 +5613,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8109,6 +8121,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11735,6 +11748,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -12481,7 +12495,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -12646,6 +12660,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -13822,6 +13837,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13939,6 +13955,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14189,6 +14206,7 @@ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -14307,6 +14325,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/package.json b/package.json index e663cbee..58a65a71 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "mathjs": "^11.9.0", "merge-audio-buffers": "^1.0.0", "papaparse": "^5.4.1", + "retry": "^0.13.1", "stats.js": "^0.17.0", "sweetalert2": "^11.14.0", "virtual-keypad": "^5.15.2", diff --git a/preprocess/gitlabUtils.ts b/preprocess/gitlabUtils.ts index e7f5eb1e..3c402713 100644 --- a/preprocess/gitlabUtils.ts +++ b/preprocess/gitlabUtils.ts @@ -86,7 +86,7 @@ const retryWithCondition = async ( } } }; -export const getRetryDelayMs = (attempt: number) => { +const getRetryDelayMs = (attempt: number) => { const delaySec = Math.min( BASE_DELAY_SEC * Math.pow(1.75, attempt), MAX_DELAY_SEC, @@ -136,7 +136,7 @@ const fetchAllPages = async (apiUrl: string, options: RequestInit) => { } return responses; }; -export class User { +class User { public username = ""; public name = ""; public id = ""; @@ -187,7 +187,7 @@ export class User { } } -export const copyUser = (user: User): User => { +const copyUser = (user: User): User => { const newUser = new User(user.accessToken); newUser.username = user.username; newUser.name = user.name; @@ -201,7 +201,7 @@ export const copyUser = (user: User): User => { }; // https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions -export interface ICommitAction { +interface ICommitAction { action: "create" | "delete" | "move" | "update" | "chmod"; file_path: string; content?: string; @@ -225,7 +225,7 @@ export interface ICommitAction { * @param oldProjectList [Optional] * @returns returns list of all gitlab projects created by user */ -export const getAllProjects = async ( +const getAllProjects = async ( user: User, oldProjectList: any[] = [], ) => { @@ -295,7 +295,7 @@ export const getAllProjects = async ( * @param keyProjectName project name to search for * @returns project with given project name */ -export const getProjectByNameInProjectList = ( +const getProjectByNameInProjectList = ( projectList: any[], keyProjectName: string, ): any => { @@ -307,7 +307,7 @@ export const getProjectByNameInProjectList = ( * @param keyProjectName project name to search for * @returns true if keyProjectName exists in given project list (or Promise if projectList is a Promise) */ -export const isProjectNameExistInProjectList = ( +const isProjectNameExistInProjectList = ( projectList: any[] | Promise, keyProjectName: string, ): boolean | Promise => { @@ -342,7 +342,7 @@ export const isProjectNameExistInProjectList = ( * @param gitlabUser project will be created on behalf of this userFont * @returns API response */ -export const createEmptyRepo = async ( +const createEmptyRepo = async ( repoName: string, user: User, ): Promise => { @@ -359,7 +359,7 @@ export const createEmptyRepo = async ( return newRepoData; }; -export const setRepoName = async ( +const setRepoName = async ( user: User, name: string, ): Promise => { @@ -401,7 +401,7 @@ const complianceProjectName = (name: string): string => { /* -------------------------------------------------------------------------- */ -export interface Repository { +interface Repository { id: string; } @@ -409,7 +409,7 @@ export interface Repository { * @param user queried user * @returns names of resource files in common "EasyEyesResources" repository (fonts and forms) */ -export const getCommonResourcesNames = async ( +const getCommonResourcesNames = async ( user: User, ): Promise<{ [key: string]: string[] }> => { const resolvedProjectList = await user.projectList; @@ -460,7 +460,7 @@ export const getCommonResourcesNames = async ( return resourcesNameByType; }; -export const downloadCommonResources = async ( +const downloadCommonResources = async ( user: User, projectRepoId: string, experimentFileName: string, @@ -569,7 +569,7 @@ export const downloadCommonResources = async ( }); }; -export const getProlificToken = async (user: User): Promise => { +const getProlificToken = async (user: User): Promise => { const resolvedProjectList = await user.projectList; const easyEyesResourcesRepo = getProjectByNameInProjectList( resolvedProjectList, @@ -792,7 +792,7 @@ const deleteFiles = async ( ); }; -export const getCompatibilityRequirementsForProject = async ( +const getCompatibilityRequirementsForProject = async ( user: User, repoName: string, ): Promise => { @@ -843,7 +843,7 @@ export const getCompatibilityRequirementsForProject = async ( return response; }; -export const getDurationForProject = async ( +const getDurationForProject = async ( user: User, repoName: string, ): Promise => { @@ -896,7 +896,7 @@ export const getDurationForProject = async ( return response; }; -export const getOriginalFileNameForProject = async ( +const getOriginalFileNameForProject = async ( user: User, repoName: string, ): Promise => { @@ -943,7 +943,7 @@ interface RecruitmentServiceInformation { recruitmentProlificWorkspace: boolean | null; } -export const getPastProlificIdFromExperimentTables = async ( +const getPastProlificIdFromExperimentTables = async ( user: User, repoName: string, fileName: string, @@ -998,7 +998,7 @@ export const getPastProlificIdFromExperimentTables = async ( return prolificProjectId; }; -export const getRecruitmentServiceConfig = async ( +const getRecruitmentServiceConfig = async ( user: User, repoName: string, ): Promise => { @@ -1159,7 +1159,7 @@ async function splitCSVAndZip( /** * Download data folder as a ZIP file from GitLab repository */ -export const downloadDataFolder = async ( +const downloadDataFolder = async ( user: User, project: any, prolificStudyId: string, @@ -1477,7 +1477,7 @@ const preprocessDataframe = (df: any) => { return df.head(1); }; // read experiment data folder and return a list of dataframes -export const getExperimentDataFrames = async (user: User, project: any) => { +const getExperimentDataFrames = async (user: User, project: any) => { const headers = new Headers(); headers.append("Authorization", `bearer ${user.accessToken}`); const requestOptions: any = { @@ -1522,7 +1522,7 @@ export const getExperimentDataFrames = async (user: User, project: any) => { }; // fetch data folder -export const getdataFolder = async (user: User, project: any) => { +const getdataFolder = async (user: User, project: any) => { const headers = new Headers(); headers.append("Authorization", `bearer ${user.accessToken}`); const perPage = 100; @@ -1557,7 +1557,7 @@ export const getdataFolder = async (user: User, project: any) => { return allData; }; -export const getDataFolderCsvLength = async (user: User, project: any) => { +const getDataFolderCsvLength = async (user: User, project: any) => { let dataFolder = await getdataFolder(user, project); // Use project.last_activity_at as the last date let latestDate: Date | false = false; @@ -1627,7 +1627,11 @@ export const getDataFolderCsvLength = async (user: User, project: any) => { return dataFolder ? [dataFolder.length, formattedLatestDate] : [0, false]; }; -export const createResourcesRepo = async (user: User): Promise => { +/** + * Primary API: Creates the EasyEyesResources repository for a user. + * This is the main entry point for this module. + */ +const createResourcesRepo = async (user: User): Promise => { const commonResourcesRepo = await createEmptyRepo(resourcesRepoName, user); if (!commonResourcesRepo) throw new Error( @@ -1656,7 +1660,7 @@ export const createResourcesRepo = async (user: User): Promise => { * @param user target user * @param resourceFileList list of all resources to be uploaded */ -export const createOrUpdateCommonResources = async ( +const createOrUpdateCommonResources = async ( user: User, resourceFileList: File[], ): Promise => { @@ -1765,7 +1769,7 @@ export const createOrUpdateCommonResources = async ( ); }; -export const createOrUpdateProlificToken = async ( +const createOrUpdateProlificToken = async ( user: User, token: string, ): Promise => { @@ -1801,7 +1805,7 @@ export const createOrUpdateProlificToken = async ( * makes given commits to Gitlab repository * @returns response from API call made to push commits */ -export const pushCommits = async ( +const pushCommits = async ( user: User, repo: Repository, commits: ICommitAction[], @@ -1842,7 +1846,7 @@ export const pushCommits = async ( return await response; }; -export const commitMessages = { +const commitMessages = { newResourcesUploaded: "⚡ new EasyEyes resources", resourcesTransferred: "📦 load EasyEyes resources from resources repo", thresholdCoreFileUploaded: "🔮 create threshold core components", @@ -1852,13 +1856,13 @@ export const commitMessages = { addProlificStudyId: "📝 add Prolific study id for the experiment", }; -export const defaultBranch = "master"; +const defaultBranch = "master"; /* -------------------------------------------------------------------------- */ /* -------------------------- CORE CREATE NEW REPO -------------------------- */ /* -------------------------------------------------------------------------- */ -export const getGitlabBodyForThreshold = async ( +const getGitlabBodyForThreshold = async ( startIndex: number, endIndex: number, user: User, @@ -1893,7 +1897,7 @@ export const getGitlabBodyForThreshold = async ( return res; }; -export const getGitlabBodyForTypekitKit = async (kitId: string) => { +const getGitlabBodyForTypekitKit = async (kitId: string) => { const res: ICommitAction[] = []; res.push({ action: "create", @@ -1907,7 +1911,7 @@ export const getGitlabBodyForTypekitKit = async (kitId: string) => { return res; }; -export const getGitlabBodyForCompatibilityRequirementFile = async ( +const getGitlabBodyForCompatibilityRequirementFile = async ( req: object, ) => { const res: ICommitAction[] = []; @@ -1940,7 +1944,7 @@ export const getGitlabBodyForCompatibilityRequirementFile = async ( return res; }; -export const getGitlabBodyForDurationText = (req: object) => { +const getGitlabBodyForDurationText = (req: object) => { const res: ICommitAction[] = []; const content = JSON.stringify(req); res.push({ @@ -1952,7 +1956,7 @@ export const getGitlabBodyForDurationText = (req: object) => { return res; }; -export const getGitlabBodyForExperimentLanguage = (language: string) => { +const getGitlabBodyForExperimentLanguage = (language: string) => { const res: ICommitAction[] = []; const content = `const experimentLanguage = "${language}"`; res.push({ @@ -2336,7 +2340,7 @@ const createRequestedResourcesOnRepo = async ( ); }; -export const manuallySetSwalTitle = (title: string) => { +const manuallySetSwalTitle = (title: string) => { const swal2Title = document.getElementById("swal2-title"); if (!swal2Title) return false; swal2Title.innerHTML = title; @@ -2517,7 +2521,7 @@ const _createExperimentTask_uploadFiles = async ( return successful; }; -export const createPavloviaExperiment = async ( +const createPavloviaExperiment = async ( user: User, projectName: string, callback: (newRepo: any, experimentUrl: string, serviceUrl: string) => void, @@ -2564,7 +2568,7 @@ export const createPavloviaExperiment = async ( /* -------------------------------------------------------------------------- */ -export const runExperiment = async ( +const runExperiment = async ( user: User, newRepo: Repository, experimentUrl: string, @@ -2601,7 +2605,7 @@ export const runExperiment = async ( } }; -export const getExperimentStatus = async (user: User, newRepo: Repository) => { +const getExperimentStatus = async (user: User, newRepo: Repository) => { const running = await fetch( "https://pavlovia.org/api/v2/experiments/" + newRepo.id, { @@ -2617,7 +2621,7 @@ export const getExperimentStatus = async (user: User, newRepo: Repository) => { return result.experiment.status2; }; -export const setExperimentSaveFormat = async ( +const setExperimentSaveFormat = async ( user: User, newRepo: Repository, ) => { @@ -2651,7 +2655,7 @@ export const setExperimentSaveFormat = async ( /* -------------------------------------------------------------------------- */ -export const generateAndUploadCompletionURL = async ( +const generateAndUploadCompletionURL = async ( user: User, newRepo: any, handleUpdateUser: (user: User) => void, @@ -2747,7 +2751,7 @@ export const generateAndUploadCompletionURL = async ( * @param gitlabRepo target repository * @param user gitlabRepo is owned by this user */ -export const createProlificStudyIdFile = async ( +const createProlificStudyIdFile = async ( gitlabRepo: Repository, user: User, studyId: string, @@ -2771,7 +2775,7 @@ export const createProlificStudyIdFile = async ( }; // fetch prolific study-id -export const getProlificStudyId = async (user: User, id: any) => { +const getProlificStudyId = async (user: User, id: any) => { if (!id) { return ""; } @@ -2799,3 +2803,93 @@ export const getProlificStudyId = async (user: User, id: any) => { if (response.includes("404 File Not Found")) return ""; return response; }; + +/* -------------------------------------------------------------------------- */ +/* EXPORTS */ +/* -------------------------------------------------------------------------- */ + +/** + * Primary API - Default Export + * Main function for creating the EasyEyesResources repository + */ +export default createResourcesRepo; + +/** + * Core utilities - User management and authentication + */ +export { + User, // User class for authentication and project management + copyUser, // Create a copy of a User instance +}; + +/** + * Project management functions + */ +export { + getAllProjects, // Get all projects for a user + getProjectByNameInProjectList, // Find project by name in list + isProjectNameExistInProjectList, // Check if project exists +}; + +/** + * Repository naming and management + */ +export { + setRepoName, // Generate unique repository name +}; + +/** + * Resource management functions + */ +export { + getCommonResourcesNames, // Get resource file names from repo + createOrUpdateCommonResources, // Upload/update resources to repo + downloadCommonResources, // Download resources from repo +}; + +/** + * Prolific integration functions + */ +export { + getProlificToken, // Fetch Prolific API token + createOrUpdateProlificToken, // Upload/update Prolific token + getProlificStudyId, // Get Prolific study ID + createProlificStudyIdFile, // Create file with Prolific study ID +}; + +/** + * Experiment creation and management + */ +export { + createPavloviaExperiment, // Create new Pavlovia experiment + runExperiment, // Set experiment to RUNNING status + getExperimentStatus, // Get current experiment status + setExperimentSaveFormat, // Set experiment data save format + generateAndUploadCompletionURL, // Generate completion URLs for recruitment +}; + +/** + * Project metadata functions + */ +export { + getCompatibilityRequirementsForProject, // Get compatibility requirements + getDurationForProject, // Get experiment duration + getOriginalFileNameForProject, // Get original experiment file name + getRecruitmentServiceConfig, // Get recruitment service configuration +}; + +/** + * Data download and retrieval functions + */ +export { + downloadDataFolder, // Download experiment data as ZIP + getDataFolderCsvLength, // Get data folder statistics +}; + +/** + * UI and utility functions + */ +export { + manuallySetSwalTitle, // Set SweetAlert title manually + getRetryDelayMs, // Calculate retry delay for exponential backoff +}; diff --git a/preprocess/user.ts b/preprocess/user.ts index 6f3345c1..c744ae27 100644 --- a/preprocess/user.ts +++ b/preprocess/user.ts @@ -1,5 +1,7 @@ +// Import the primary API (default export) +import createResourcesRepo from "./gitlabUtils"; +// Import supporting utilities (named exports) import { - createResourcesRepo, getProlificToken, getCommonResourcesNames, isProjectNameExistInProjectList,