coursecode 0.1.0
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/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file navigation-validators.js
|
|
3
|
+
* @description Validation functions for navigation access control.
|
|
4
|
+
* All validators return consistent {allowed: boolean, message: string|null, reason?: string} objects.
|
|
5
|
+
* @author Seth
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { evaluateGatingCondition, shouldBypassGating } from './navigation-helpers.js';
|
|
10
|
+
import * as AssessmentManager from '../managers/assessment-manager.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Determines whether a slide should be included in the sequential navigation flow.
|
|
14
|
+
* Checks sequence configuration and evaluates dynamic conditions.
|
|
15
|
+
*
|
|
16
|
+
* SMART DEFAULT: For slides with gating conditions AND hidden from menu (menu.hidden: true),
|
|
17
|
+
* the gating conditions are automatically used for sequence inclusion. This prevents
|
|
18
|
+
* navigation loops when gating is bypassed for testing, without requiring duplicate config.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} slide - The slide entry containing navigation configuration
|
|
21
|
+
* @param {object} stateManager - StateManager instance for reading state
|
|
22
|
+
* @param {Map} assessmentConfigs - Map of assessment configurations
|
|
23
|
+
* @returns {boolean} True when the slide should be part of the active sequence
|
|
24
|
+
*/
|
|
25
|
+
export function isSlideInSequence(slide, stateManager, assessmentConfigs) {
|
|
26
|
+
if (!slide) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sequence = slide.navigation?.sequence;
|
|
31
|
+
const gating = slide.navigation?.gating;
|
|
32
|
+
const isHiddenFromMenu = slide.menu?.hidden === true;
|
|
33
|
+
|
|
34
|
+
// SMART DEFAULT: If no explicit sequence config, but slide has gating AND is hidden,
|
|
35
|
+
// use gating conditions to determine sequence inclusion.
|
|
36
|
+
// This prevents loops when gating is bypassed (e.g., remedial slides).
|
|
37
|
+
if (!sequence && gating?.conditions?.length > 0 && isHiddenFromMenu) {
|
|
38
|
+
const gatingMode = gating.mode || 'all';
|
|
39
|
+
if (gatingMode === 'any') {
|
|
40
|
+
return gating.conditions.some(condition =>
|
|
41
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
// Default to 'all' for any mode value (including invalid ones)
|
|
45
|
+
return gating.conditions.every(condition =>
|
|
46
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// No sequence config and no smart default applies = always included
|
|
51
|
+
if (!sequence) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const includeWhen = sequence.includeWhen || [];
|
|
56
|
+
const skipUntil = sequence.skipUntil || [];
|
|
57
|
+
const includeByDefault = sequence.includeByDefault !== false;
|
|
58
|
+
|
|
59
|
+
let include = includeByDefault;
|
|
60
|
+
|
|
61
|
+
// Check includeWhen conditions
|
|
62
|
+
if (includeWhen.length > 0) {
|
|
63
|
+
include = includeWhen.every(condition =>
|
|
64
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check skipUntil conditions
|
|
69
|
+
if (include && skipUntil.length > 0) {
|
|
70
|
+
const skipSatisfied = skipUntil.every(condition =>
|
|
71
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
72
|
+
);
|
|
73
|
+
if (!skipSatisfied) {
|
|
74
|
+
include = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return include;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates whether a slide is accessible based on gating conditions.
|
|
83
|
+
* Checks all configured gating rules and applies dev mode override.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} slide - The slide entry with navigation configuration
|
|
86
|
+
* @param {object} stateManager - StateManager instance
|
|
87
|
+
* @param {Map} assessmentConfigs - Map of assessment configurations
|
|
88
|
+
* @returns {{allowed: boolean, message: string|null, reason?: string}} Access result
|
|
89
|
+
*/
|
|
90
|
+
export function validateSlideAccess(slide, stateManager, assessmentConfigs) {
|
|
91
|
+
// Dev mode gating override
|
|
92
|
+
if (shouldBypassGating()) {
|
|
93
|
+
return { allowed: true, message: null, reason: 'dev-bypass' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!slide || !slide.navigation || !slide.navigation.gating) {
|
|
97
|
+
return { allowed: true, message: null, reason: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const gating = slide.navigation.gating;
|
|
101
|
+
const conditions = gating.conditions || [];
|
|
102
|
+
|
|
103
|
+
if (conditions.length === 0) {
|
|
104
|
+
return { allowed: true, message: null, reason: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const mode = gating.mode || 'all';
|
|
108
|
+
let allowed;
|
|
109
|
+
|
|
110
|
+
if (mode === 'any') {
|
|
111
|
+
// At least one condition must be met
|
|
112
|
+
allowed = conditions.some(condition =>
|
|
113
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
// Default to 'all' — all conditions must be met (including invalid mode values)
|
|
117
|
+
allowed = conditions.every(condition =>
|
|
118
|
+
evaluateGatingCondition(condition, stateManager, assessmentConfigs)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
allowed,
|
|
124
|
+
message: allowed ? null : (gating.message || 'This content is currently locked.'),
|
|
125
|
+
reason: allowed ? null : 'gating-failed'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validates whether navigation FROM the current slide is allowed.
|
|
131
|
+
* Checks assessment completion requirements.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} slide - The slide entry to check
|
|
134
|
+
* @param {Map} assessmentConfigs - Map of assessment configurations
|
|
135
|
+
* @returns {{allowed: boolean, message: string|null, reason?: string}} Navigation permission result
|
|
136
|
+
*/
|
|
137
|
+
export function validateNavigationFrom(slide, assessmentConfigs) {
|
|
138
|
+
// Dev mode override
|
|
139
|
+
if (shouldBypassGating()) {
|
|
140
|
+
return { allowed: true, message: null, reason: 'dev-bypass' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!slide) {
|
|
144
|
+
return { allowed: true, message: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if this is an assessment with completion requirements that block navigation
|
|
148
|
+
if (slide.type === 'assessment') {
|
|
149
|
+
const config = assessmentConfigs.get(slide.assessmentId);
|
|
150
|
+
const requirements = config?.completionRequirements;
|
|
151
|
+
|
|
152
|
+
// Only block if blockNavigation is explicitly true
|
|
153
|
+
if (requirements?.blockNavigation === true) {
|
|
154
|
+
const requirementsMet = AssessmentManager.meetsCompletionRequirements(
|
|
155
|
+
slide.assessmentId,
|
|
156
|
+
requirements
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!requirementsMet) {
|
|
160
|
+
// Build helpful message based on what's required
|
|
161
|
+
let message = 'You must complete the assessment before continuing.';
|
|
162
|
+
if (requirements.requirePass) {
|
|
163
|
+
message = 'You must pass the assessment before continuing.';
|
|
164
|
+
} else if (requirements.requireSubmission) {
|
|
165
|
+
message = 'You must submit your assessment before continuing.';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { allowed: false, message, reason: 'assessment-incomplete' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { allowed: true, message: null };
|
|
174
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file state/index.js
|
|
3
|
+
* @description Barrel export for the state module.
|
|
4
|
+
* stateManager is the sole public API — all LMS and state operations flow through it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { default } from './state-manager.js';
|
|
8
|
+
export { formatISO8601Duration } from './state-manager.js';
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lms-connection.js
|
|
3
|
+
* @description Manages LMS connection lifecycle using format-specific drivers.
|
|
4
|
+
* Handles initialization, termination, keep-alive, and emergency save on unload.
|
|
5
|
+
* Provides semantic passthrough to the active driver.
|
|
6
|
+
*
|
|
7
|
+
* INTERNAL: This is an internal module. External code should use stateManager
|
|
8
|
+
* as the sole public API for state operations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from '../utilities/logger.js';
|
|
12
|
+
import { eventBus } from '../core/event-bus.js';
|
|
13
|
+
import stateManager from './state-manager.js';
|
|
14
|
+
import { createDriver } from '../drivers/driver-factory.js';
|
|
15
|
+
import { classifyLmsError } from './lms-error-utils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the LMS format using runtime detection.
|
|
19
|
+
* Priority: <meta name="lms-format"> → build-time env → 'cmi5' default.
|
|
20
|
+
*
|
|
21
|
+
* The meta tag is the primary mechanism — it's stamped into index.html at
|
|
22
|
+
* build/packaging time so a single universal build can serve any format.
|
|
23
|
+
* The cloud (or ZIP packaging) can re-stamp it without re-running Vite.
|
|
24
|
+
*/
|
|
25
|
+
function getLMSFormat() {
|
|
26
|
+
// 1. Runtime: <meta name="lms-format"> in the HTML (stamped at build or by cloud)
|
|
27
|
+
if (typeof document !== 'undefined') {
|
|
28
|
+
const metaEl = document.querySelector('meta[name="lms-format"]');
|
|
29
|
+
if (metaEl?.content) {
|
|
30
|
+
return metaEl.content;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Build-time: Vite define (still useful for preview server / dev builds)
|
|
35
|
+
if (import.meta.env.LMS_FORMAT) {
|
|
36
|
+
return import.meta.env.LMS_FORMAT;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Default
|
|
40
|
+
return 'cmi5';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class LMSConnection {
|
|
44
|
+
constructor() {
|
|
45
|
+
// Create the appropriate driver based on format
|
|
46
|
+
this.format = getLMSFormat();
|
|
47
|
+
this.driver = null; // Lazy initialization via async _getDriver()
|
|
48
|
+
|
|
49
|
+
// Keep-alive interval handle
|
|
50
|
+
this.keepAliveInterval = null;
|
|
51
|
+
|
|
52
|
+
// Session timing
|
|
53
|
+
this.sessionStartTime = 0;
|
|
54
|
+
|
|
55
|
+
// Compatibility profile (influences timeout behavior and guardrails)
|
|
56
|
+
this.compatibilityMode = 'auto';
|
|
57
|
+
|
|
58
|
+
// Operation diagnostics
|
|
59
|
+
this.diagnostics = {
|
|
60
|
+
profile: 'balanced',
|
|
61
|
+
lastSuccessAt: null,
|
|
62
|
+
operationCounts: {
|
|
63
|
+
commitSuccess: 0,
|
|
64
|
+
commitFailure: 0,
|
|
65
|
+
terminateSuccess: 0,
|
|
66
|
+
terminateFailure: 0
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets the driver instance, creating it lazily if needed.
|
|
73
|
+
* Must be called after initialize() for non-SCORM2004 formats.
|
|
74
|
+
* @returns {LMSDriver} The driver instance
|
|
75
|
+
* @throws {Error} If driver not initialized
|
|
76
|
+
*/
|
|
77
|
+
_getDriver() {
|
|
78
|
+
if (!this.driver) {
|
|
79
|
+
throw new Error('[LMSConnection] Driver not initialized. Call initialize() first.');
|
|
80
|
+
}
|
|
81
|
+
return this.driver;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Public API
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the current LMS format.
|
|
90
|
+
* @returns {string} 'scorm2004' | 'scorm1.2' | 'cmi5' | 'lti'
|
|
91
|
+
*/
|
|
92
|
+
getFormat() {
|
|
93
|
+
return this.format;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Checks if connected to LMS.
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
get isConnected() {
|
|
101
|
+
return this.driver?.isConnected() ?? false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Checks if session is terminated.
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
get isTerminated() {
|
|
109
|
+
return this.driver?.isTerminated() ?? false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Gets session start time.
|
|
114
|
+
* @returns {number} Timestamp
|
|
115
|
+
*/
|
|
116
|
+
get sessionStart() {
|
|
117
|
+
return this.sessionStartTime;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Initializes the LMS connection.
|
|
122
|
+
* @returns {Promise<boolean>} True if connected
|
|
123
|
+
*/
|
|
124
|
+
async initialize() {
|
|
125
|
+
this.sessionStartTime = Date.now();
|
|
126
|
+
|
|
127
|
+
// Create driver asynchronously (allows dynamic import for cmi5/scorm12)
|
|
128
|
+
this.driver = await createDriver(this.format);
|
|
129
|
+
logger.debug(`[LMSConnection] Created ${this.format} driver`);
|
|
130
|
+
|
|
131
|
+
const result = await this.driver.initialize();
|
|
132
|
+
|
|
133
|
+
if (result) {
|
|
134
|
+
this._startKeepAlive();
|
|
135
|
+
logger.debug(`[LMSConnection] Initialized with ${this.format} driver`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Terminates the LMS connection.
|
|
143
|
+
* @returns {Promise<boolean>} True if successful
|
|
144
|
+
*/
|
|
145
|
+
async terminate() {
|
|
146
|
+
this._stopKeepAlive();
|
|
147
|
+
|
|
148
|
+
const driver = this._getDriver();
|
|
149
|
+
const timeoutMs = this._getOperationTimeoutMs('terminate');
|
|
150
|
+
try {
|
|
151
|
+
const result = await this._withTimeout(driver.terminate(), timeoutMs, 'terminate');
|
|
152
|
+
this._markOperationSuccess('terminate');
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this._markOperationFailure('terminate', error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gets driver capabilities.
|
|
162
|
+
* @returns {Object} Capabilities declaration
|
|
163
|
+
*/
|
|
164
|
+
getCapabilities() {
|
|
165
|
+
return this._getDriver().getCapabilities();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Semantic Reads (passthrough to driver) ---
|
|
169
|
+
|
|
170
|
+
getEntryMode() {
|
|
171
|
+
return this._getDriver().getEntryMode();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getBookmark() {
|
|
175
|
+
return this._getDriver().getBookmark();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getCompletion() {
|
|
179
|
+
return this._getDriver().getCompletion();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getSuccess() {
|
|
183
|
+
return this._getDriver().getSuccess();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getScore() {
|
|
187
|
+
return this._getDriver().getScore();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getLearnerInfo() {
|
|
191
|
+
return this._getDriver().getLearnerInfo();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Semantic Writes (passthrough to driver) ---
|
|
195
|
+
|
|
196
|
+
setBookmark(location) {
|
|
197
|
+
return this._getDriver().setBookmark(location);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
reportScore(score) {
|
|
201
|
+
return this._getDriver().reportScore(score);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
reportCompletion(status) {
|
|
205
|
+
return this._getDriver().reportCompletion(status);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
reportSuccess(status) {
|
|
209
|
+
return this._getDriver().reportSuccess(status);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
reportProgress(measure) {
|
|
213
|
+
return this._getDriver().reportProgress(measure);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
reportSessionTime(duration) {
|
|
217
|
+
return this._getDriver().reportSessionTime(duration);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
reportObjective(objective) {
|
|
221
|
+
return this._getDriver().reportObjective(objective);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
reportInteraction(interaction) {
|
|
225
|
+
return this._getDriver().reportInteraction(interaction);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setExitMode(mode) {
|
|
229
|
+
return this._getDriver().setExitMode(mode);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Data Persistence ---
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Commits buffered writes to the LMS.
|
|
236
|
+
* @returns {Promise<boolean>} True if successful
|
|
237
|
+
*/
|
|
238
|
+
async commit() {
|
|
239
|
+
const timeoutMs = this._getOperationTimeoutMs('commit');
|
|
240
|
+
try {
|
|
241
|
+
const result = await this._withTimeout(this._getDriver().commit(), timeoutMs, 'commit');
|
|
242
|
+
this._markOperationSuccess('commit');
|
|
243
|
+
return result;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this._markOperationFailure('commit', error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Gets parsed suspend_data from the LMS.
|
|
252
|
+
* @returns {object|null} Parsed suspend data
|
|
253
|
+
*/
|
|
254
|
+
getSuspendData() {
|
|
255
|
+
return this._getDriver().getSuspendData();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Sets suspend_data in the LMS.
|
|
260
|
+
* @param {object} data - The data to store
|
|
261
|
+
* @returns {boolean} True if successful
|
|
262
|
+
*/
|
|
263
|
+
setSuspendData(data) {
|
|
264
|
+
return this._getDriver().setSuspendData(data);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Gets cmi5 launch data (moveOn, masteryScore, etc.).
|
|
269
|
+
* Only available for cmi5 format.
|
|
270
|
+
* @returns {Object|null} Launch data or null if not available/not cmi5
|
|
271
|
+
*/
|
|
272
|
+
getLaunchData() {
|
|
273
|
+
const driver = this.driver;
|
|
274
|
+
if (!driver || typeof driver.getLaunchData !== 'function') {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return driver.getLaunchData();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Gets the underlying driver instance.
|
|
282
|
+
* Used by xAPI statement service to access driver-specific methods.
|
|
283
|
+
* @returns {LMSDriver|null} The driver instance or null if not initialized
|
|
284
|
+
*/
|
|
285
|
+
getDriver() {
|
|
286
|
+
return this.driver;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Sets compatibility mode to tune reliability behavior by LMS profile.
|
|
291
|
+
* Must be called before initialize() for deterministic startup behavior.
|
|
292
|
+
* @param {'auto'|'balanced'|'strict-scorm12'|'conservative-scorm2004'|'modern-http'} mode
|
|
293
|
+
*/
|
|
294
|
+
setCompatibilityMode(mode = 'auto') {
|
|
295
|
+
const validModes = new Set([
|
|
296
|
+
'auto',
|
|
297
|
+
'balanced',
|
|
298
|
+
'strict-scorm12',
|
|
299
|
+
'conservative-scorm2004',
|
|
300
|
+
'modern-http'
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
if (!validModes.has(mode)) {
|
|
304
|
+
throw new Error(`[LMSConnection] Invalid compatibility mode "${mode}". Expected one of: ${Array.from(validModes).join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.compatibilityMode = mode;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
getCompatibilityMode() {
|
|
311
|
+
return this.compatibilityMode;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getDiagnostics() {
|
|
315
|
+
return { ...this.diagnostics, operationCounts: { ...this.diagnostics.operationCounts } };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Lifecycle Handlers
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Sets up lifecycle handlers for page unload/hide events.
|
|
324
|
+
*/
|
|
325
|
+
setupLifecycleHandlers() {
|
|
326
|
+
// Layer 4: Auto-Terminate on Unload (Emergency Save)
|
|
327
|
+
window.addEventListener('pagehide', () => {
|
|
328
|
+
if (this.isTerminated) return;
|
|
329
|
+
|
|
330
|
+
// Startup Guard: Ignore pagehide events < 2s after startup
|
|
331
|
+
if (Date.now() - this.sessionStartTime < 2000) {
|
|
332
|
+
logger.debug('[LMSConnection] Page unload detected immediately after startup (< 2s). Ignoring.');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
logger.debug('[LMSConnection] Page unload detected. Attempting emergency save...');
|
|
337
|
+
|
|
338
|
+
// Use emergencySave() for any driver that supports it (sendBeacon guaranteed delivery)
|
|
339
|
+
if (this.driver?.emergencySave) {
|
|
340
|
+
try {
|
|
341
|
+
this.driver.emergencySave();
|
|
342
|
+
logger.debug('[LMSConnection] Emergency save completed via sendBeacon');
|
|
343
|
+
} catch (e) {
|
|
344
|
+
logger.debug('[LMSConnection] Emergency save failed:', e.message);
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// For SCORM formats, use synchronous exitCourseWithSuspend
|
|
350
|
+
try {
|
|
351
|
+
// Delegate to StateManager to save exit status
|
|
352
|
+
const exitPromise = stateManager.exitCourseWithSuspend();
|
|
353
|
+
if (exitPromise && typeof exitPromise.catch === 'function') {
|
|
354
|
+
exitPromise.catch(e => {
|
|
355
|
+
logger.debug('[LMSConnection] Best-effort save during unload did not complete:', e.message);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
logger.debug('[LMSConnection] Best-effort save during unload did not complete:', e.message);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// Private Helpers
|
|
366
|
+
// ============================================================================
|
|
367
|
+
|
|
368
|
+
_startKeepAlive() {
|
|
369
|
+
this._stopKeepAlive();
|
|
370
|
+
|
|
371
|
+
// Only SCORM formats need keep-alive (cmi5/LTI use stateless HTTP)
|
|
372
|
+
if (this.format.startsWith('cmi5') || this.format === 'lti') {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const KEEP_ALIVE_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
|
377
|
+
|
|
378
|
+
this.keepAliveInterval = setInterval(() => {
|
|
379
|
+
if (!this.isConnected || this.isTerminated) {
|
|
380
|
+
this._stopKeepAlive();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Delegate to driver's ping method
|
|
384
|
+
this.driver?.ping?.();
|
|
385
|
+
}, KEEP_ALIVE_INTERVAL);
|
|
386
|
+
|
|
387
|
+
logger.debug('[LMSConnection] Keep-alive mechanism started (10m interval)');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_stopKeepAlive() {
|
|
391
|
+
if (this.keepAliveInterval) {
|
|
392
|
+
clearInterval(this.keepAliveInterval);
|
|
393
|
+
this.keepAliveInterval = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_resolveCompatibilityProfile() {
|
|
398
|
+
if (this.compatibilityMode !== 'auto') {
|
|
399
|
+
return this.compatibilityMode;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (this.format === 'scorm1.2') return 'strict-scorm12';
|
|
403
|
+
if (this.format === 'scorm2004') return 'conservative-scorm2004';
|
|
404
|
+
if (this.format.startsWith('cmi5') || this.format === 'lti') return 'modern-http';
|
|
405
|
+
return 'balanced';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_getOperationTimeoutMs(operation) {
|
|
409
|
+
const profile = this._resolveCompatibilityProfile();
|
|
410
|
+
this.diagnostics.profile = profile;
|
|
411
|
+
|
|
412
|
+
const timeoutMatrix = {
|
|
413
|
+
balanced: { commit: 8000, terminate: 10000 },
|
|
414
|
+
'strict-scorm12': { commit: 5000, terminate: 7000 },
|
|
415
|
+
'conservative-scorm2004': { commit: 7000, terminate: 9000 },
|
|
416
|
+
'modern-http': { commit: 12000, terminate: 15000 }
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const profileTimeouts = timeoutMatrix[profile] || timeoutMatrix.balanced;
|
|
420
|
+
return profileTimeouts[operation] || 8000;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async _withTimeout(promise, timeoutMs, operation) {
|
|
424
|
+
let timeoutHandle;
|
|
425
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
426
|
+
timeoutHandle = setTimeout(() => {
|
|
427
|
+
reject(new Error(`[LMSConnection] ${operation} timed out after ${timeoutMs}ms`));
|
|
428
|
+
}, timeoutMs);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
433
|
+
} finally {
|
|
434
|
+
clearTimeout(timeoutHandle);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_markOperationSuccess(operation) {
|
|
439
|
+
this.diagnostics.lastSuccessAt = new Date().toISOString();
|
|
440
|
+
if (operation === 'commit') {
|
|
441
|
+
this.diagnostics.operationCounts.commitSuccess++;
|
|
442
|
+
} else if (operation === 'terminate') {
|
|
443
|
+
this.diagnostics.operationCounts.terminateSuccess++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
_markOperationFailure(operation, error) {
|
|
448
|
+
const classification = classifyLmsError(error);
|
|
449
|
+
const message = error?.message || String(error);
|
|
450
|
+
const context = {
|
|
451
|
+
domain: 'lms',
|
|
452
|
+
operation,
|
|
453
|
+
classification,
|
|
454
|
+
format: this.format,
|
|
455
|
+
profile: this.diagnostics.profile,
|
|
456
|
+
stack: error?.stack
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
if (operation === 'commit') {
|
|
460
|
+
this.diagnostics.operationCounts.commitFailure++;
|
|
461
|
+
} else if (operation === 'terminate') {
|
|
462
|
+
this.diagnostics.operationCounts.terminateFailure++;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
logger.error(`[LMSConnection] ${operation} failed`, {
|
|
466
|
+
...context,
|
|
467
|
+
message
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
eventBus.emit('lms:operationFailed', {
|
|
471
|
+
operation,
|
|
472
|
+
classification,
|
|
473
|
+
message,
|
|
474
|
+
format: context.format,
|
|
475
|
+
profile: context.profile
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const lmsConnection = new LMSConnection();
|
|
481
|
+
export default lmsConnection;
|
|
482
|
+
export { LMSConnection };
|