@learnpack/learnpack 5.0.364 → 5.0.366
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/serve.js +39 -14
- package/lib/managers/config/index.js +2 -5
- package/lib/managers/file.js +19 -51
- package/lib/managers/ideReleases.d.ts +24 -0
- package/lib/managers/ideReleases.js +111 -0
- package/lib/utils/continueGeneration.d.ts +65 -0
- package/lib/utils/continueGeneration.js +82 -0
- package/lib/utils/rigoActions.d.ts +4 -1
- package/lib/utils/rigoActions.js +3 -1
- package/package.json +1 -1
- package/src/commands/serve.ts +69 -22
- package/src/managers/config/index.ts +17 -31
- package/src/managers/file.ts +30 -70
- package/src/managers/ideReleases.ts +142 -0
- package/src/ui/_app/app.js +130 -72
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/continueGeneration.ts +108 -0
- package/src/utils/rigoActions.ts +5 -1
package/lib/commands/serve.js
CHANGED
|
@@ -25,6 +25,7 @@ const dotenv = require("dotenv");
|
|
|
25
25
|
const creatorUtilities_1 = require("../utils/creatorUtilities");
|
|
26
26
|
const lessonRecovery_1 = require("../utils/lessonRecovery");
|
|
27
27
|
const generationRetry_1 = require("../utils/generationRetry");
|
|
28
|
+
const continueGeneration_1 = require("../utils/continueGeneration");
|
|
28
29
|
// import { handleAssetCreation } from "./publish"
|
|
29
30
|
const axios_1 = require("axios");
|
|
30
31
|
const FormData = require("form-data");
|
|
@@ -1518,6 +1519,28 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
1518
1519
|
if (!exercise.id || !exercise.title) {
|
|
1519
1520
|
throw new errorHandler_1.ValidationError(`Lesson "${lessonUid}" is missing required properties (id or title)`);
|
|
1520
1521
|
}
|
|
1522
|
+
const language = syllabus.courseInfo.language || "en";
|
|
1523
|
+
const previousStatus = exercise.status;
|
|
1524
|
+
// A second click, or a retry while the job still runs, would start
|
|
1525
|
+
// another paid job for the same lesson.
|
|
1526
|
+
if (previousStatus === "GENERATING") {
|
|
1527
|
+
const jobId = (0, continueGeneration_1.currentJobId)(exercise, language);
|
|
1528
|
+
let job = null;
|
|
1529
|
+
if (jobId) {
|
|
1530
|
+
try {
|
|
1531
|
+
job = await (0, rigoActions_1.getCompletionJob)(rigoToken, jobId, {
|
|
1532
|
+
timeoutMs: 5000,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
console.error(`Could not check job ${jobId}:`, error);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if ((0, continueGeneration_1.isGenerationRunning)(exercise, language, job, Date.now())) {
|
|
1540
|
+
console.log(`Lesson ${lessonUid} is already being generated (job ${jobId}), not starting another one`);
|
|
1541
|
+
throw new errorHandler_1.ConflictError(`Lesson "${exercise.title}" is already being generated`, { code: "LESSON_ALREADY_GENERATING", completionId: jobId });
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1521
1544
|
// previous exercise
|
|
1522
1545
|
let previousReadme = "---";
|
|
1523
1546
|
const previousExercise = syllabus.lessons[exerciseIndex - 1];
|
|
@@ -1543,7 +1566,7 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
1543
1566
|
// A new job by the user, so the automatic retries start over.
|
|
1544
1567
|
syllabus.lessons[exerciseIndex].status = "GENERATING";
|
|
1545
1568
|
syllabus.lessons[exerciseIndex].translations = {
|
|
1546
|
-
[
|
|
1569
|
+
[language]: startedJob(completionId),
|
|
1547
1570
|
};
|
|
1548
1571
|
console.log("Lesson", syllabus.lessons[exerciseIndex]);
|
|
1549
1572
|
if (syllabus.feedback && typeof syllabus.feedback === "string") {
|
|
@@ -1552,7 +1575,7 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
1552
1575
|
else {
|
|
1553
1576
|
syllabus.feedback = feedback;
|
|
1554
1577
|
}
|
|
1555
|
-
syllabus.generationMode = mode;
|
|
1578
|
+
syllabus.generationMode = (0, continueGeneration_1.resolveGenerationMode)(previousStatus, syllabus.generationMode, mode);
|
|
1556
1579
|
await saveSyllabus(courseSlug, syllabus, bucket);
|
|
1557
1580
|
});
|
|
1558
1581
|
res.json({ status: "SUCCESS" });
|
|
@@ -3763,9 +3786,12 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3763
3786
|
await uploadInitialReadme(bucket, exSlug, targetDir, syllabus.courseInfo);
|
|
3764
3787
|
}
|
|
3765
3788
|
const sidebar = await (0, sidebar_1.createInitialSidebar)(syllabus.lessons.map(lesson => (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title)), syllabus.courseInfo.language);
|
|
3789
|
+
const language = syllabus.courseInfo.language || "en";
|
|
3766
3790
|
const initialSyllabus = Object.assign(Object.assign({}, syllabus), { lessons: syllabus.lessons.map((lesson, index) => {
|
|
3767
3791
|
if (index < 1) {
|
|
3768
|
-
|
|
3792
|
+
// Its start time, so a retry right away is refused even before
|
|
3793
|
+
// the job id is saved below.
|
|
3794
|
+
return Object.assign(Object.assign({}, lesson), { generated: false, status: "GENERATING", translations: { [language]: startedJob(0) } });
|
|
3769
3795
|
}
|
|
3770
3796
|
return Object.assign(Object.assign({}, lesson), { generated: false, status: "PENDING" });
|
|
3771
3797
|
}) });
|
|
@@ -3773,17 +3799,16 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3773
3799
|
await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
|
|
3774
3800
|
const firstLesson = syllabus.lessons[0];
|
|
3775
3801
|
const lastResult = "---";
|
|
3776
|
-
//
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
}
|
|
3802
|
+
// In the course queue, so the webhook of this job waits until its id
|
|
3803
|
+
// is saved. The id was never saved before, so the first lesson had no
|
|
3804
|
+
// protection against a second job.
|
|
3805
|
+
await serializeByCourse(courseSlug, async () => {
|
|
3806
|
+
// Use new two-phase generation workflow
|
|
3807
|
+
const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
|
|
3808
|
+
await patchLesson(courseSlug, firstLesson.uid, bucket, lesson => {
|
|
3809
|
+
lesson.translations = { [language]: startedJob(completionId) };
|
|
3810
|
+
});
|
|
3811
|
+
});
|
|
3787
3812
|
await createInitialReadme(JSON.stringify(syllabus.courseInfo), courseSlug, rigoToken);
|
|
3788
3813
|
return res.json({
|
|
3789
3814
|
message: "Course generation started",
|
|
@@ -12,11 +12,10 @@ const sidebarGenerator_1 = require("../../utils/sidebarGenerator");
|
|
|
12
12
|
const defaults_1 = require("./defaults");
|
|
13
13
|
const exercise_1 = require("./exercise");
|
|
14
14
|
const file_1 = require("../file");
|
|
15
|
+
const ideReleases_1 = require("../ideReleases");
|
|
15
16
|
/* exercise folder name standard */
|
|
16
17
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
17
18
|
// eslint-disable-next-line
|
|
18
|
-
const fetch = require("node-fetch");
|
|
19
|
-
// eslint-disable-next-line
|
|
20
19
|
const chalk = require("chalk");
|
|
21
20
|
/* exercise folder name standard */
|
|
22
21
|
/**
|
|
@@ -186,9 +185,7 @@ exports.default = async ({ grading, mode, disableGrading, version, }) => {
|
|
|
186
185
|
configObj.config.editor.version = version;
|
|
187
186
|
else if (configObj.config.editor.version === null) {
|
|
188
187
|
console_1.default.debug("Config version not found, downloading default.");
|
|
189
|
-
|
|
190
|
-
const packageJSON = await resp.json();
|
|
191
|
-
configObj.config.editor.version = packageJSON.version || "5.0.0";
|
|
188
|
+
configObj.config.editor.version = await (0, ideReleases_1.getLatestEditorVersion)();
|
|
192
189
|
}
|
|
193
190
|
configObj.config.dirPath = "./" + confPath.base;
|
|
194
191
|
configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
|
package/lib/managers/file.js
CHANGED
|
@@ -9,6 +9,7 @@ const targz = require("targz");
|
|
|
9
9
|
const console_1 = require("../utils/console");
|
|
10
10
|
const https = require("https");
|
|
11
11
|
const errors_1 = require("../utils/errors");
|
|
12
|
+
const ideReleases_1 = require("./ideReleases");
|
|
12
13
|
// eslint-disable-next-line
|
|
13
14
|
const fetch = require("node-fetch");
|
|
14
15
|
const decompress = (sourcePath, destinationPath) => new Promise((resolve, reject) => {
|
|
@@ -30,54 +31,22 @@ const decompress = (sourcePath, destinationPath) => new Promise((resolve, reject
|
|
|
30
31
|
exports.decompress = decompress;
|
|
31
32
|
const downloadEditor = async (version, destination) => {
|
|
32
33
|
console_1.default.info("Downloading the LearnPack coding UI, this may take a minute...");
|
|
33
|
-
if (!version)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
if (!version)
|
|
35
|
+
version = await (0, ideReleases_1.getLatestEditorVersion)();
|
|
36
|
+
const [cloudTags, githubTags] = await Promise.all([
|
|
37
|
+
(0, ideReleases_1.fetchTagList)(ideReleases_1.IDE_CLOUDFRONT_MANIFEST),
|
|
38
|
+
(0, ideReleases_1.fetchTagList)(ideReleases_1.IDE_GITHUB_MANIFEST),
|
|
39
|
+
]);
|
|
40
|
+
let release = (0, ideReleases_1.resolveEditorRelease)(version, cloudTags, githubTags);
|
|
41
|
+
if (!release) {
|
|
42
|
+
console_1.default.debug(`No IDE version found for ${version} in the versions manifests, defaulting to version 4.0.2`);
|
|
43
|
+
release = { version: "4.0.2", source: "github" };
|
|
39
44
|
}
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
tags = await tagsRes.json();
|
|
46
|
-
if (!Array.isArray(tags)) {
|
|
47
|
-
throw new TypeError(`Invalid versions.json format`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
console_1.default.debug("Error reading versions.json, defaulting to version 4.0.2", error);
|
|
52
|
-
version = "4.0.2";
|
|
53
|
-
tags = [`learnpack-${version}.tar.gz`];
|
|
54
|
-
}
|
|
55
|
-
let matchingTags;
|
|
56
|
-
try {
|
|
57
|
-
if (tags.length === 0 || !tags)
|
|
58
|
-
throw (0, errors_1.InternalError)(`No found tags in versions.json ${tags}`);
|
|
59
|
-
matchingTags = tags
|
|
60
|
-
.filter(tag => tag.includes(`learnpack-${versionPrefix}`))
|
|
61
|
-
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
62
|
-
if (matchingTags.length === 0)
|
|
63
|
-
throw (0, errors_1.InternalError)(`No matching version found for prefix ${versionPrefix} in the versions.json`);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
console_1.default.debug("Error processing tags, defaulting to version 4.0.2", error);
|
|
67
|
-
version = "4.0.2";
|
|
68
|
-
matchingTags = [`learnpack-${version}.tar.gz`];
|
|
69
|
-
}
|
|
70
|
-
const latestVersion = matchingTags[0]
|
|
71
|
-
.replace("learnpack-", "")
|
|
72
|
-
.replace(".tar.gz", "");
|
|
73
|
-
const versionNumber = parseInt(latestVersion.split(".")[0]);
|
|
74
|
-
let url = `https://github.com/learnpack/coding-ide/blob/${latestVersion}/dist`;
|
|
75
|
-
if (versionNumber > 2)
|
|
76
|
-
url = `https://github.com/learnpack/ide/blob/master/bin/learnpack-${latestVersion}.tar.gz`;
|
|
77
|
-
const resp2 = await fetch(url, { method: "HEAD" });
|
|
78
|
-
if (!resp2.ok)
|
|
79
|
-
throw (0, errors_1.InternalError)(`Coding Editor ${latestVersion} was not found on learnpack repository, check the config.editor.version property on learn.json`);
|
|
80
|
-
url += versionNumber > 2 ? `?raw=true` : `/app.tar.gz?raw=true`;
|
|
45
|
+
const url = (0, ideReleases_1.editorTarballUrl)(release.version, release.source);
|
|
46
|
+
const resp = await fetch(url, { method: "HEAD", timeout: ideReleases_1.FETCH_TIMEOUT_MS });
|
|
47
|
+
if (!resp.ok)
|
|
48
|
+
throw (0, errors_1.InternalError)(`Coding Editor ${release.version} was not found on ${release.source}, check the config.editor.version property on learn.json`);
|
|
49
|
+
console_1.default.debug(`Using IDE ${release.version} from ${release.source}`);
|
|
81
50
|
return (0, exports.download)(url, destination);
|
|
82
51
|
};
|
|
83
52
|
exports.downloadEditor = downloadEditor;
|
|
@@ -92,7 +61,7 @@ const download = (url, dest) => {
|
|
|
92
61
|
});
|
|
93
62
|
file.on("error", err => {
|
|
94
63
|
file.close();
|
|
95
|
-
if (err.
|
|
64
|
+
if (err.code === "EEXIST") {
|
|
96
65
|
console_1.default.debug("File already exists");
|
|
97
66
|
resolve("File already exists");
|
|
98
67
|
}
|
|
@@ -103,11 +72,10 @@ const download = (url, dest) => {
|
|
|
103
72
|
});
|
|
104
73
|
response.pipe(file);
|
|
105
74
|
}
|
|
106
|
-
else if (
|
|
107
|
-
// Console.debug("Servers redirected to "+response.headers.location)
|
|
75
|
+
else if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
|
108
76
|
// Recursively follow redirects, only a 200 will resolve.
|
|
109
77
|
if (response.headers.location) {
|
|
110
|
-
(0, exports.download)(response.headers.location, dest)
|
|
78
|
+
(0, exports.download)(new URL(response.headers.location, url).toString(), dest)
|
|
111
79
|
.then(() => resolve(/* */ ""))
|
|
112
80
|
.catch(error => {
|
|
113
81
|
console_1.default.error(error);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const IDE_CLOUDFRONT_BASE = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide";
|
|
2
|
+
export declare const IDE_CLOUDFRONT_MANIFEST = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide/versions.json";
|
|
3
|
+
export declare const IDE_GITHUB_MANIFEST = "https://raw.githubusercontent.com/learnpack/ide/master/versions.json";
|
|
4
|
+
export declare const IDE_GITHUB_PACKAGE = "https://raw.githubusercontent.com/learnpack/ide/master/package.json";
|
|
5
|
+
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
6
|
+
export type TEditorSource = "cloudfront" | "github";
|
|
7
|
+
export interface IEditorRelease {
|
|
8
|
+
version: string;
|
|
9
|
+
source: TEditorSource;
|
|
10
|
+
}
|
|
11
|
+
interface IParsedVersion {
|
|
12
|
+
version: string;
|
|
13
|
+
major: number;
|
|
14
|
+
minor: number;
|
|
15
|
+
patch: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const parseVersion: (version: string) => IParsedVersion | null;
|
|
18
|
+
export declare const parseTag: (tag: string) => IParsedVersion | null;
|
|
19
|
+
export declare const highestVersion: (tags: string[] | null) => string | null;
|
|
20
|
+
export declare const resolveEditorRelease: (requested: string, cloudTags: string[] | null, githubTags: string[] | null) => IEditorRelease | null;
|
|
21
|
+
export declare const editorTarballUrl: (version: string, source: TEditorSource) => string;
|
|
22
|
+
export declare const fetchTagList: (url: string) => Promise<string[] | null>;
|
|
23
|
+
export declare const getLatestEditorVersion: () => Promise<string>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLatestEditorVersion = exports.fetchTagList = exports.editorTarballUrl = exports.resolveEditorRelease = exports.highestVersion = exports.parseTag = exports.parseVersion = exports.FETCH_TIMEOUT_MS = exports.IDE_GITHUB_PACKAGE = exports.IDE_GITHUB_MANIFEST = exports.IDE_CLOUDFRONT_MANIFEST = exports.IDE_CLOUDFRONT_BASE = void 0;
|
|
4
|
+
const console_1 = require("../utils/console");
|
|
5
|
+
// eslint-disable-next-line
|
|
6
|
+
const fetch = require("node-fetch");
|
|
7
|
+
// IDE tarballs published from 5.0.417 on live on CloudFront; older ones stay in the
|
|
8
|
+
// learnpack/ide GitHub repo, whose versions.json is frozen. Both lists are merged.
|
|
9
|
+
exports.IDE_CLOUDFRONT_BASE = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide";
|
|
10
|
+
exports.IDE_CLOUDFRONT_MANIFEST = `${exports.IDE_CLOUDFRONT_BASE}/versions.json`;
|
|
11
|
+
exports.IDE_GITHUB_MANIFEST = "https://raw.githubusercontent.com/learnpack/ide/master/versions.json";
|
|
12
|
+
exports.IDE_GITHUB_PACKAGE = "https://raw.githubusercontent.com/learnpack/ide/master/package.json";
|
|
13
|
+
exports.FETCH_TIMEOUT_MS = 10000;
|
|
14
|
+
const DEFAULT_EDITOR_VERSION = "5.0.0";
|
|
15
|
+
const parseVersion = (version) => {
|
|
16
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
17
|
+
if (!match)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
version: match.slice(1, 4).join("."),
|
|
21
|
+
major: Number(match[1]),
|
|
22
|
+
minor: Number(match[2]),
|
|
23
|
+
patch: Number(match[3]),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
exports.parseVersion = parseVersion;
|
|
27
|
+
// "learnpack-5.0.417.tar.gz" -> parsed 5.0.417; anything else -> null
|
|
28
|
+
const parseTag = (tag) => {
|
|
29
|
+
const match = /^learnpack-(\d+\.\d+\.\d+)\.tar\.gz$/.exec(tag);
|
|
30
|
+
return match ? (0, exports.parseVersion)(match[1]) : null;
|
|
31
|
+
};
|
|
32
|
+
exports.parseTag = parseTag;
|
|
33
|
+
const compareVersions = (a, b) => a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
34
|
+
const parseTags = (tags) => (tags || []).map(exports.parseTag).filter((v) => v !== null);
|
|
35
|
+
// Highest version listed in a manifest, or null when there is none.
|
|
36
|
+
const highestVersion = (tags) => {
|
|
37
|
+
const sorted = parseTags(tags).sort(compareVersions);
|
|
38
|
+
return sorted.length > 0 ? sorted[sorted.length - 1].version : null;
|
|
39
|
+
};
|
|
40
|
+
exports.highestVersion = highestVersion;
|
|
41
|
+
// Picks the tarball to download for the requested version. Only major.minor of the
|
|
42
|
+
// request is used and the highest patch available wins, as the CLI always did.
|
|
43
|
+
// The source is CloudFront when the version is in its manifest, GitHub otherwise.
|
|
44
|
+
const resolveEditorRelease = (requested, cloudTags, githubTags) => {
|
|
45
|
+
const [major, minor] = requested.split(".").map(part => Number(part));
|
|
46
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor))
|
|
47
|
+
return null;
|
|
48
|
+
const cloud = parseTags(cloudTags);
|
|
49
|
+
const candidates = [...cloud, ...parseTags(githubTags)]
|
|
50
|
+
.filter(v => v.major === major && v.minor === minor)
|
|
51
|
+
.sort(compareVersions);
|
|
52
|
+
if (candidates.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const { version } = candidates[candidates.length - 1];
|
|
55
|
+
const source = cloud.some(v => v.version === version) ?
|
|
56
|
+
"cloudfront" :
|
|
57
|
+
"github";
|
|
58
|
+
return { version, source };
|
|
59
|
+
};
|
|
60
|
+
exports.resolveEditorRelease = resolveEditorRelease;
|
|
61
|
+
const editorTarballUrl = (version, source) => {
|
|
62
|
+
if (source === "cloudfront") {
|
|
63
|
+
return `${exports.IDE_CLOUDFRONT_BASE}/learnpack-${version}.tar.gz`;
|
|
64
|
+
}
|
|
65
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
66
|
+
return major > 2 ?
|
|
67
|
+
`https://github.com/learnpack/ide/blob/master/bin/learnpack-${version}.tar.gz?raw=true` :
|
|
68
|
+
`https://github.com/learnpack/coding-ide/blob/${version}/dist/app.tar.gz?raw=true`;
|
|
69
|
+
};
|
|
70
|
+
exports.editorTarballUrl = editorTarballUrl;
|
|
71
|
+
// Fetches a versions manifest. Never throws: any failure returns null.
|
|
72
|
+
const fetchTagList = async (url) => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(url, { timeout: exports.FETCH_TIMEOUT_MS });
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
console_1.default.debug(`Could not read ${url}: ${res.status} ${res.statusText}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const json = await res.json();
|
|
80
|
+
if (!Array.isArray(json) || !json.every(tag => typeof tag === "string")) {
|
|
81
|
+
console_1.default.debug(`Invalid versions manifest format at ${url}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console_1.default.debug(`Could not read ${url}`, error);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
exports.fetchTagList = fetchTagList;
|
|
92
|
+
// Latest published IDE version: CloudFront manifest, then GitHub package.json, then a default.
|
|
93
|
+
const getLatestEditorVersion = async () => {
|
|
94
|
+
const fromCloud = (0, exports.highestVersion)(await (0, exports.fetchTagList)(exports.IDE_CLOUDFRONT_MANIFEST));
|
|
95
|
+
if (fromCloud)
|
|
96
|
+
return fromCloud;
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(exports.IDE_GITHUB_PACKAGE, { timeout: exports.FETCH_TIMEOUT_MS });
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
const json = await res.json();
|
|
101
|
+
if (typeof (json === null || json === void 0 ? void 0 : json.version) === "string" && (0, exports.parseVersion)(json.version)) {
|
|
102
|
+
return json.version;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console_1.default.debug("Could not read the IDE package.json from GitHub", error);
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_EDITOR_VERSION;
|
|
110
|
+
};
|
|
111
|
+
exports.getLatestEditorVersion = getLatestEditorVersion;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules for `POST /actions/continue-generating`, the request behind the
|
|
3
|
+
* creator's continue and retry buttons. Without them, every extra click starts
|
|
4
|
+
* another paid job for a lesson that is already being generated.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* How long a PENDING job counts as running. Each phase of a lesson takes 1 to
|
|
8
|
+
* 3 minutes, so a job PENDING for longer is most likely stuck, for example
|
|
9
|
+
* after a worker restart. A stuck job holds the whole course, because the next
|
|
10
|
+
* lesson only starts after this one finishes. Replacing a slow job that was
|
|
11
|
+
* still alive only costs one extra job: serve ignores the older job's webhook.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RUNNING_JOB_MAX_AGE_MS: number;
|
|
14
|
+
/**
|
|
15
|
+
* How long a finished job counts as running. Rigobot saves the result before
|
|
16
|
+
* it sends the webhook, and serve may still be handling it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const WEBHOOK_GRACE_MS: number;
|
|
19
|
+
/**
|
|
20
|
+
* How long a lesson counts as running when its job cannot be checked: it has
|
|
21
|
+
* no job id yet, or Rigobot did not answer.
|
|
22
|
+
*/
|
|
23
|
+
export declare const UNCHECKED_JOB_GRACE_MS: number;
|
|
24
|
+
export type GenerationMode = "next-three" | "continue-with-all";
|
|
25
|
+
type LessonState = {
|
|
26
|
+
status?: string;
|
|
27
|
+
translations?: {
|
|
28
|
+
[language: string]: {
|
|
29
|
+
completionId?: number;
|
|
30
|
+
startedAt?: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/** The fields of a Rigobot completion job these rules read. */
|
|
35
|
+
export type JobState = {
|
|
36
|
+
status?: string;
|
|
37
|
+
ended_at?: string | null;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The job to check before starting another one.
|
|
41
|
+
* @param lesson - The lesson as saved in the syllabus.
|
|
42
|
+
* @param language - The course language.
|
|
43
|
+
* @returns The id of the job the lesson waits for, or null if it has none.
|
|
44
|
+
*/
|
|
45
|
+
export declare const currentJobId: (lesson: LessonState, language: string) => number | null;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a lesson is still being generated, so a new job must not start.
|
|
48
|
+
* @param lesson - The lesson as saved in the syllabus.
|
|
49
|
+
* @param language - The course language.
|
|
50
|
+
* @param job - Its job as Rigobot reports it, or null if it was not checked.
|
|
51
|
+
* @param now - The current time in milliseconds.
|
|
52
|
+
* @returns True when the lesson must wait for its current job.
|
|
53
|
+
*/
|
|
54
|
+
export declare const isGenerationRunning: (lesson: LessonState, language: string, job: JobState | null, now: number) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The generation mode to save. The user only picks a mode on a PENDING
|
|
57
|
+
* lesson. The retry buttons offer no choice, so a retry keeps the mode of the
|
|
58
|
+
* course instead of resetting it.
|
|
59
|
+
* @param lessonStatus - The status of the lesson before this request.
|
|
60
|
+
* @param current - The mode saved in the syllabus.
|
|
61
|
+
* @param requested - The mode in the request.
|
|
62
|
+
* @returns The mode to save, or undefined when the course has none.
|
|
63
|
+
*/
|
|
64
|
+
export declare const resolveGenerationMode: (lessonStatus: string | undefined, current: unknown, requested: unknown) => GenerationMode | undefined;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rules for `POST /actions/continue-generating`, the request behind the
|
|
4
|
+
* creator's continue and retry buttons. Without them, every extra click starts
|
|
5
|
+
* another paid job for a lesson that is already being generated.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.resolveGenerationMode = exports.isGenerationRunning = exports.currentJobId = exports.UNCHECKED_JOB_GRACE_MS = exports.WEBHOOK_GRACE_MS = exports.RUNNING_JOB_MAX_AGE_MS = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* How long a PENDING job counts as running. Each phase of a lesson takes 1 to
|
|
11
|
+
* 3 minutes, so a job PENDING for longer is most likely stuck, for example
|
|
12
|
+
* after a worker restart. A stuck job holds the whole course, because the next
|
|
13
|
+
* lesson only starts after this one finishes. Replacing a slow job that was
|
|
14
|
+
* still alive only costs one extra job: serve ignores the older job's webhook.
|
|
15
|
+
*/
|
|
16
|
+
exports.RUNNING_JOB_MAX_AGE_MS = 5 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* How long a finished job counts as running. Rigobot saves the result before
|
|
19
|
+
* it sends the webhook, and serve may still be handling it.
|
|
20
|
+
*/
|
|
21
|
+
exports.WEBHOOK_GRACE_MS = 90 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* How long a lesson counts as running when its job cannot be checked: it has
|
|
24
|
+
* no job id yet, or Rigobot did not answer.
|
|
25
|
+
*/
|
|
26
|
+
exports.UNCHECKED_JOB_GRACE_MS = 2 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* The job to check before starting another one.
|
|
29
|
+
* @param lesson - The lesson as saved in the syllabus.
|
|
30
|
+
* @param language - The course language.
|
|
31
|
+
* @returns The id of the job the lesson waits for, or null if it has none.
|
|
32
|
+
*/
|
|
33
|
+
const currentJobId = (lesson, language) => {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
const id = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.completionId;
|
|
36
|
+
return typeof id === "number" && id > 0 ? id : null;
|
|
37
|
+
};
|
|
38
|
+
exports.currentJobId = currentJobId;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a lesson is still being generated, so a new job must not start.
|
|
41
|
+
* @param lesson - The lesson as saved in the syllabus.
|
|
42
|
+
* @param language - The course language.
|
|
43
|
+
* @param job - Its job as Rigobot reports it, or null if it was not checked.
|
|
44
|
+
* @param now - The current time in milliseconds.
|
|
45
|
+
* @returns True when the lesson must wait for its current job.
|
|
46
|
+
*/
|
|
47
|
+
const isGenerationRunning = (lesson, language, job, now) => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
if (lesson.status !== "GENERATING")
|
|
50
|
+
return false;
|
|
51
|
+
const startedAt = (_b = (_a = lesson.translations) === null || _a === void 0 ? void 0 : _a[language]) === null || _b === void 0 ? void 0 : _b.startedAt;
|
|
52
|
+
const startedAgo = typeof startedAt === "number" && startedAt > 0 ?
|
|
53
|
+
now - startedAt :
|
|
54
|
+
Number.POSITIVE_INFINITY;
|
|
55
|
+
if (!job)
|
|
56
|
+
return startedAgo < exports.UNCHECKED_JOB_GRACE_MS;
|
|
57
|
+
if (job.status === "PENDING")
|
|
58
|
+
return startedAgo < exports.RUNNING_JOB_MAX_AGE_MS;
|
|
59
|
+
// A finished job. Its webhook may still be on its way to serve.
|
|
60
|
+
const endedAt = job.ended_at ? Date.parse(job.ended_at) : Number.NaN;
|
|
61
|
+
if (Number.isNaN(endedAt))
|
|
62
|
+
return startedAgo < exports.UNCHECKED_JOB_GRACE_MS;
|
|
63
|
+
return now - endedAt < exports.WEBHOOK_GRACE_MS;
|
|
64
|
+
};
|
|
65
|
+
exports.isGenerationRunning = isGenerationRunning;
|
|
66
|
+
/**
|
|
67
|
+
* The generation mode to save. The user only picks a mode on a PENDING
|
|
68
|
+
* lesson. The retry buttons offer no choice, so a retry keeps the mode of the
|
|
69
|
+
* course instead of resetting it.
|
|
70
|
+
* @param lessonStatus - The status of the lesson before this request.
|
|
71
|
+
* @param current - The mode saved in the syllabus.
|
|
72
|
+
* @param requested - The mode in the request.
|
|
73
|
+
* @returns The mode to save, or undefined when the course has none.
|
|
74
|
+
*/
|
|
75
|
+
const resolveGenerationMode = (lessonStatus, current, requested) => {
|
|
76
|
+
const isMode = (mode) => mode === "next-three" || mode === "continue-with-all";
|
|
77
|
+
if (isMode(requested) && (lessonStatus === "PENDING" || !isMode(current))) {
|
|
78
|
+
return requested;
|
|
79
|
+
}
|
|
80
|
+
return isMode(current) ? current : undefined;
|
|
81
|
+
};
|
|
82
|
+
exports.resolveGenerationMode = resolveGenerationMode;
|
|
@@ -26,6 +26,7 @@ export type TCompletionJob = {
|
|
|
26
26
|
status: "PENDING" | "SUCCESS" | "ERROR" | string;
|
|
27
27
|
status_text?: string | null;
|
|
28
28
|
parsed?: Record<string, any> | null;
|
|
29
|
+
ended_at?: string | null;
|
|
29
30
|
};
|
|
30
31
|
type TTranslateAndWaitOptions = {
|
|
31
32
|
/** How often the job status is checked while it is still running. */
|
|
@@ -35,7 +36,9 @@ type TTranslateAndWaitOptions = {
|
|
|
35
36
|
/** Called after every successful poll, useful to keep a heartbeat alive. */
|
|
36
37
|
onPoll?: (job: TCompletionJob) => void | Promise<void>;
|
|
37
38
|
};
|
|
38
|
-
export declare const getCompletionJob: (token: string, completionId: number | string
|
|
39
|
+
export declare const getCompletionJob: (token: string, completionId: number | string, options?: {
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}) => Promise<TCompletionJob>;
|
|
39
42
|
export declare const translateExerciseAndWait: (token: string, inputs: TTranslateInputs, options?: TTranslateAndWaitOptions) => Promise<TCompletionJob>;
|
|
40
43
|
type TGenerateCourseIntroductionInputs = {
|
|
41
44
|
course_title: string;
|
package/lib/utils/rigoActions.js
CHANGED
|
@@ -114,12 +114,14 @@ const assertJobSucceeded = (job) => {
|
|
|
114
114
|
}
|
|
115
115
|
return job;
|
|
116
116
|
};
|
|
117
|
-
const getCompletionJob = async (token, completionId) => {
|
|
117
|
+
const getCompletionJob = async (token, completionId, options = {}) => {
|
|
118
118
|
const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/${completionId}/`, {
|
|
119
119
|
headers: {
|
|
120
120
|
"Content-Type": "application/json",
|
|
121
121
|
Authorization: "Token " + token,
|
|
122
122
|
},
|
|
123
|
+
// No timeout by default: the translation polling waits as long as it needs.
|
|
124
|
+
timeout: options.timeoutMs,
|
|
123
125
|
});
|
|
124
126
|
return response.data;
|
|
125
127
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learnpack/learnpack",
|
|
3
3
|
"description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
|
|
4
|
-
"version": "5.0.
|
|
4
|
+
"version": "5.0.366",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|