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,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scorm-2004-driver.js
|
|
3
|
+
* @description SCORM 2004 4th Edition driver implementation using pipwerks wrapper.
|
|
4
|
+
* Extends ScormDriverBase for shared pipwerks initialization and connection management.
|
|
5
|
+
*
|
|
6
|
+
* Handles direct communication with the LMS API (API_1484_11).
|
|
7
|
+
* Uses the industry-standard pipwerks SCORM wrapper for battle-tested
|
|
8
|
+
* API discovery across complex iframe/opener hierarchies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ScormDriverBase } from './scorm-driver-base.js';
|
|
12
|
+
import { eventBus } from '../core/event-bus.js';
|
|
13
|
+
import { logger } from '../utilities/logger.js';
|
|
14
|
+
import LZString from 'lz-string';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SCORM 2004 4th Edition Driver
|
|
18
|
+
* Communicates with window.API_1484_11 via the pipwerks SCORM wrapper.
|
|
19
|
+
*/
|
|
20
|
+
export class Scorm2004Driver extends ScormDriverBase {
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this._isRecovered = false; // Flag for "Already Initialized" recovery
|
|
24
|
+
|
|
25
|
+
// CMI cache — populated at init, updated on writes
|
|
26
|
+
this._cmiCache = {
|
|
27
|
+
entry: null,
|
|
28
|
+
bookmark: '',
|
|
29
|
+
completionStatus: 'unknown',
|
|
30
|
+
successStatus: 'unknown',
|
|
31
|
+
learnerId: '',
|
|
32
|
+
learnerName: '',
|
|
33
|
+
objectives: {}, // Cached objectives keyed by ID
|
|
34
|
+
objectiveIdToIndex: new Map(), // Maps objective ID → CMI index
|
|
35
|
+
interactions: [], // Cached interactions array
|
|
36
|
+
interactionsCount: 0 // Current count for append operations
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Interface Implementation
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
getFormat() {
|
|
45
|
+
return 'scorm2004';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getCapabilities() {
|
|
49
|
+
return {
|
|
50
|
+
supportsObjectives: true,
|
|
51
|
+
supportsInteractions: true,
|
|
52
|
+
supportsComments: true,
|
|
53
|
+
supportsEmergencySave: false,
|
|
54
|
+
maxSuspendDataBytes: 64000,
|
|
55
|
+
asyncCommit: false
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initializes the SCORM 2004 connection using pipwerks.
|
|
61
|
+
* Handles the standard Initialize() call and recovers gracefully from Error 103.
|
|
62
|
+
* @returns {Promise<boolean>} True if connected (fresh or recovered)
|
|
63
|
+
*/
|
|
64
|
+
async initialize() {
|
|
65
|
+
if (this._isConnected) {
|
|
66
|
+
logger.error('Scorm2004Driver.initialize() called more than once.', { domain: 'scorm', operation: 'Initialize (redundant call)' });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this._initPipwerks('2004');
|
|
71
|
+
|
|
72
|
+
let result = false;
|
|
73
|
+
let error = null;
|
|
74
|
+
|
|
75
|
+
// Get API handle for potential recovery
|
|
76
|
+
const api = this._scorm.API.getHandle();
|
|
77
|
+
|
|
78
|
+
// Wrap init() in try-catch because some API adapters
|
|
79
|
+
// throw exceptions instead of returning "false" when already initialized.
|
|
80
|
+
try {
|
|
81
|
+
result = this._scorm.init();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
logger.warn('[Scorm2004Driver] pipwerks init() threw an error:', e);
|
|
84
|
+
if (e.message && (e.message.includes('Already initialized') || e.message.includes('103'))) {
|
|
85
|
+
error = { code: '103', message: e.message };
|
|
86
|
+
} else {
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!result && !error) {
|
|
92
|
+
error = this._getScormError();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle Error 103 (Already Initialized) gracefully
|
|
96
|
+
if (!result || error) {
|
|
97
|
+
if (error && (error.code === '103' || error.code === 103 || (error.message && error.message.includes('Already initialized')))) {
|
|
98
|
+
logger.warn('[Scorm2004Driver] Session already initialized (Error 103). Recovering session...');
|
|
99
|
+
|
|
100
|
+
if (api) {
|
|
101
|
+
logger.debug('[Scorm2004Driver] Session is active (Error 103). Forcing recovery.');
|
|
102
|
+
|
|
103
|
+
// Mark pipwerks connection as active for recovery
|
|
104
|
+
this._scorm.connection.isActive = true;
|
|
105
|
+
|
|
106
|
+
this._isConnected = true;
|
|
107
|
+
this._isRecovered = true;
|
|
108
|
+
this._populateCache();
|
|
109
|
+
return true;
|
|
110
|
+
} else {
|
|
111
|
+
logger.error('[Scorm2004Driver] Could not get raw API handle for recovery.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const msg = error ? `SCORM initialization failed: ${error.message}` : 'SCORM initialization failed';
|
|
116
|
+
logger.error(msg, { domain: 'scorm', operation: 'Initialize', ...error });
|
|
117
|
+
throw new Error(msg);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this._isConnected = true;
|
|
121
|
+
this._populateCache();
|
|
122
|
+
logger.debug('[Scorm2004Driver] Initialize() completed successfully via pipwerks');
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Terminates the SCORM 2004 connection.
|
|
128
|
+
* Overrides base to provide detailed SCORM error reporting.
|
|
129
|
+
*/
|
|
130
|
+
async terminate() {
|
|
131
|
+
if (!this._isConnected || this._isTerminated) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const success = this._scorm.quit();
|
|
136
|
+
this._isTerminated = success;
|
|
137
|
+
|
|
138
|
+
if (!success) {
|
|
139
|
+
const error = this._getScormError();
|
|
140
|
+
const msg = error ? `SCORM termination failed: ${error.message}` : 'SCORM termination failed';
|
|
141
|
+
logger.error(msg, { domain: 'scorm', operation: 'Terminate', ...error });
|
|
142
|
+
throw new Error(msg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Commits buffered writes to the LMS.
|
|
150
|
+
* Overrides base to handle Error 103 recovery mode.
|
|
151
|
+
*/
|
|
152
|
+
async commit() {
|
|
153
|
+
this._ensureInitialized();
|
|
154
|
+
|
|
155
|
+
if (this._isTerminated) {
|
|
156
|
+
if (import.meta.env.DEV) {
|
|
157
|
+
logger.warn('[Scorm2004Driver] Ignoring commit() - SCORM session already terminated');
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this._isRecovered) {
|
|
163
|
+
return this._commitRecovered();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const success = this._scorm.save();
|
|
167
|
+
|
|
168
|
+
if (!success) {
|
|
169
|
+
logger.error('[Scorm2004Driver] Commit failed.');
|
|
170
|
+
const error = this._getScormError();
|
|
171
|
+
this._throwScormError('Commit', 'SCORM commit failed', error);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sends a keep-alive ping to the LMS to maintain session.
|
|
179
|
+
* Uses a read-only GetValue call that doesn't affect state.
|
|
180
|
+
*/
|
|
181
|
+
ping() {
|
|
182
|
+
if (!this._isConnected || this._isTerminated) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (this._isRecovered) {
|
|
188
|
+
const api = this._scorm.API.getHandle();
|
|
189
|
+
if (api) api.GetValue('cmi.mode');
|
|
190
|
+
} else {
|
|
191
|
+
this._scorm.get('cmi.mode');
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
logger.warn('[Scorm2004Driver] Keep-alive ping failed:', e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Semantic Reads
|
|
200
|
+
// ============================================================================
|
|
201
|
+
|
|
202
|
+
getEntryMode() {
|
|
203
|
+
return this._cmiCache.entry || '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getBookmark() {
|
|
207
|
+
return this._cmiCache.bookmark || '';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getCompletion() {
|
|
211
|
+
return this._cmiCache.completionStatus || 'unknown';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getSuccess() {
|
|
215
|
+
return this._cmiCache.successStatus || 'unknown';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getScore() {
|
|
219
|
+
const scaledStr = this._getValueOptional('cmi.score.scaled');
|
|
220
|
+
if (scaledStr === null) return null;
|
|
221
|
+
const scaled = parseFloat(scaledStr);
|
|
222
|
+
if (isNaN(scaled)) return null;
|
|
223
|
+
const rawStr = this._getValueOptional('cmi.score.raw');
|
|
224
|
+
const minStr = this._getValueOptional('cmi.score.min');
|
|
225
|
+
const maxStr = this._getValueOptional('cmi.score.max');
|
|
226
|
+
return {
|
|
227
|
+
scaled,
|
|
228
|
+
raw: rawStr !== null ? parseFloat(rawStr) : scaled * 100,
|
|
229
|
+
min: minStr !== null ? parseFloat(minStr) : 0,
|
|
230
|
+
max: maxStr !== null ? parseFloat(maxStr) : 100
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getLearnerInfo() {
|
|
235
|
+
return {
|
|
236
|
+
id: this._cmiCache.learnerId,
|
|
237
|
+
name: this._cmiCache.learnerName
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Semantic Writes
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
setBookmark(location) {
|
|
246
|
+
this._setValue('cmi.location', location);
|
|
247
|
+
this._cmiCache.bookmark = location;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
reportScore({ raw, scaled, min, max }) {
|
|
251
|
+
if (raw !== undefined) this._setValue('cmi.score.raw', String(raw));
|
|
252
|
+
if (scaled !== undefined) this._setValue('cmi.score.scaled', String(scaled));
|
|
253
|
+
if (min !== undefined) this._setValue('cmi.score.min', String(min));
|
|
254
|
+
if (max !== undefined) this._setValue('cmi.score.max', String(max));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
reportCompletion(status) {
|
|
258
|
+
this._setValue('cmi.completion_status', status);
|
|
259
|
+
this._cmiCache.completionStatus = status;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
reportSuccess(status) {
|
|
263
|
+
this._setValue('cmi.success_status', status);
|
|
264
|
+
this._cmiCache.successStatus = status;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
reportProgress(measure) {
|
|
268
|
+
this._setValue('cmi.progress_measure', String(measure));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
reportSessionTime(duration) {
|
|
272
|
+
this._setValue('cmi.session_time', duration);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
reportObjective(objective) {
|
|
276
|
+
if (!objective || !objective.id) return;
|
|
277
|
+
|
|
278
|
+
const index = this._getOrCreateObjectiveIndex(objective.id);
|
|
279
|
+
|
|
280
|
+
if (objective.success_status) {
|
|
281
|
+
this._setValue(`cmi.objectives.${index}.success_status`, objective.success_status);
|
|
282
|
+
}
|
|
283
|
+
if (objective.completion_status) {
|
|
284
|
+
this._setValue(`cmi.objectives.${index}.completion_status`, objective.completion_status);
|
|
285
|
+
}
|
|
286
|
+
if (objective.score !== null && objective.score !== undefined) {
|
|
287
|
+
const rawScore = objective.score;
|
|
288
|
+
const scaledScore = rawScore / 100;
|
|
289
|
+
this._setValue(`cmi.objectives.${index}.score.raw`, String(rawScore));
|
|
290
|
+
this._setValue(`cmi.objectives.${index}.score.scaled`, String(scaledScore));
|
|
291
|
+
this._setValue(`cmi.objectives.${index}.score.min`, '0');
|
|
292
|
+
this._setValue(`cmi.objectives.${index}.score.max`, '100');
|
|
293
|
+
}
|
|
294
|
+
if (objective.progress_measure !== null && objective.progress_measure !== undefined) {
|
|
295
|
+
this._setValue(`cmi.objectives.${index}.progress_measure`, String(objective.progress_measure));
|
|
296
|
+
}
|
|
297
|
+
if (objective.description) {
|
|
298
|
+
this._setValue(`cmi.objectives.${index}.description`, objective.description);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Update cache
|
|
302
|
+
this._cmiCache.objectives[objective.id] = { ...objective };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
reportInteraction(interaction) {
|
|
306
|
+
if (!interaction || !interaction.id || !interaction.type) {
|
|
307
|
+
throw new Error('Scorm2004Driver: interaction.id and interaction.type are required');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const index = this._cmiCache.interactionsCount;
|
|
311
|
+
|
|
312
|
+
// Write required fields
|
|
313
|
+
this._setValue(`cmi.interactions.${index}.id`, interaction.id);
|
|
314
|
+
this._setValue(`cmi.interactions.${index}.type`, interaction.type);
|
|
315
|
+
|
|
316
|
+
// Write optional fields
|
|
317
|
+
if (interaction.learner_response !== undefined && interaction.learner_response !== null && interaction.learner_response !== '') {
|
|
318
|
+
this._setValue(`cmi.interactions.${index}.learner_response`, interaction.learner_response);
|
|
319
|
+
}
|
|
320
|
+
if (interaction.result) {
|
|
321
|
+
this._setValue(`cmi.interactions.${index}.result`, interaction.result);
|
|
322
|
+
}
|
|
323
|
+
if (interaction.timestamp) {
|
|
324
|
+
this._setValue(`cmi.interactions.${index}.timestamp`, interaction.timestamp);
|
|
325
|
+
}
|
|
326
|
+
if (interaction.description) {
|
|
327
|
+
this._setValue(`cmi.interactions.${index}.description`, interaction.description);
|
|
328
|
+
}
|
|
329
|
+
if (interaction.weighting !== undefined && interaction.weighting !== null) {
|
|
330
|
+
this._setValue(`cmi.interactions.${index}.weighting`, String(interaction.weighting));
|
|
331
|
+
}
|
|
332
|
+
if (interaction.latency) {
|
|
333
|
+
this._setValue(`cmi.interactions.${index}.latency`, interaction.latency);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Write correct_responses
|
|
337
|
+
if (interaction.correct_responses && Array.isArray(interaction.correct_responses)) {
|
|
338
|
+
interaction.correct_responses.forEach((item, patternIndex) => {
|
|
339
|
+
const patternValue = (typeof item === 'object' && item !== null && 'pattern' in item)
|
|
340
|
+
? item.pattern
|
|
341
|
+
: item;
|
|
342
|
+
this._setValue(`cmi.interactions.${index}.correct_responses.${patternIndex}.pattern`, patternValue);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Write objectives
|
|
347
|
+
if (interaction.objectives && Array.isArray(interaction.objectives)) {
|
|
348
|
+
interaction.objectives.forEach((objectiveId, objIndex) => {
|
|
349
|
+
this._setValue(`cmi.interactions.${index}.objectives.${objIndex}.id`, objectiveId);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Update cache
|
|
354
|
+
const result = { ...interaction, _index: index };
|
|
355
|
+
this._cmiCache.interactions.push(result);
|
|
356
|
+
this._cmiCache.interactionsCount++;
|
|
357
|
+
|
|
358
|
+
logger.debug(`[Scorm2004Driver] Appended interaction "${interaction.id}" at index ${index}`);
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setExitMode(mode) {
|
|
363
|
+
// 'suspend' → 'suspend', 'normal' → '' (empty string = normal exit)
|
|
364
|
+
this._setValue('cmi.exit', mode === 'suspend' ? 'suspend' : '');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Suspend Data
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Gets and parses suspend_data from the LMS.
|
|
373
|
+
* @returns {object|null} Parsed suspend data or null
|
|
374
|
+
*/
|
|
375
|
+
getSuspendData() {
|
|
376
|
+
const data = this._getValue('cmi.suspend_data');
|
|
377
|
+
|
|
378
|
+
logger.debug('[Scorm2004Driver] getSuspendData() called');
|
|
379
|
+
logger.debug(`[Scorm2004Driver] Raw suspend_data length: ${data ? data.length : 0}`);
|
|
380
|
+
|
|
381
|
+
if (!data) {
|
|
382
|
+
logger.debug('[Scorm2004Driver] No suspend_data found');
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Decompress lz-string data
|
|
387
|
+
const jsonString = LZString.decompressFromUTF16(data);
|
|
388
|
+
if (!jsonString) {
|
|
389
|
+
logger.warn('[Scorm2004Driver] Failed to decompress suspend_data - may be corrupted');
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(jsonString);
|
|
395
|
+
logger.debug(`[Scorm2004Driver] Successfully parsed suspend_data with ${Object.keys(parsed).length} domain(s)`);
|
|
396
|
+
return parsed;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const msg = `Failed to parse suspend data as JSON: ${error.message}`;
|
|
399
|
+
logger.error(msg, { domain: 'scorm', operation: 'GetSuspendData', stack: error.stack });
|
|
400
|
+
throw new Error(msg);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Sets suspend_data in the LMS.
|
|
406
|
+
* @param {object} data - The data object to store
|
|
407
|
+
* @returns {boolean} True if successful
|
|
408
|
+
*/
|
|
409
|
+
setSuspendData(data) {
|
|
410
|
+
if (data === undefined || data === null) {
|
|
411
|
+
const msg = 'Cannot set suspend data: data is null or undefined';
|
|
412
|
+
logger.error(msg, { domain: 'scorm', operation: 'SetSuspendData', dataType: typeof data });
|
|
413
|
+
throw new Error(msg);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let serialized;
|
|
417
|
+
let dataSnapshot;
|
|
418
|
+
try {
|
|
419
|
+
dataSnapshot = {
|
|
420
|
+
topLevelKeys: Object.keys(data),
|
|
421
|
+
dataType: typeof data,
|
|
422
|
+
isArray: Array.isArray(data),
|
|
423
|
+
keyCount: Object.keys(data).length
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
serialized = JSON.stringify(data);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const msg = `Failed to serialize suspend data to JSON: ${error.message}`;
|
|
429
|
+
logger.error(msg, { domain: 'scorm', operation: 'SetSuspendData', stack: error.stack, dataSnapshot });
|
|
430
|
+
throw new Error(msg);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (typeof serialized !== 'string') {
|
|
434
|
+
const msg = `JSON.stringify returned invalid type: ${typeof serialized}`;
|
|
435
|
+
logger.error(msg, { domain: 'scorm', operation: 'SetSuspendData' });
|
|
436
|
+
throw new Error(msg);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Compress using lz-string
|
|
440
|
+
const compressed = LZString.compressToUTF16(serialized);
|
|
441
|
+
const originalSizeKB = (serialized.length / 1024).toFixed(2);
|
|
442
|
+
const compressedSizeKB = (compressed.length / 1024).toFixed(2);
|
|
443
|
+
const compressionRatio = ((1 - compressed.length / serialized.length) * 100).toFixed(1);
|
|
444
|
+
|
|
445
|
+
logger.debug(`[Scorm2004Driver] Compressed suspend_data: ${originalSizeKB}KB → ${compressedSizeKB}KB (${compressionRatio}% reduction)`);
|
|
446
|
+
|
|
447
|
+
// Emit size event for monitoring
|
|
448
|
+
eventBus.emit('suspend-data:size', {
|
|
449
|
+
bytes: compressed.length,
|
|
450
|
+
kilobytes: parseFloat(compressedSizeKB),
|
|
451
|
+
originalBytes: serialized.length,
|
|
452
|
+
originalKilobytes: parseFloat(originalSizeKB),
|
|
453
|
+
compressionRatio: parseFloat(compressionRatio)
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Progressive warnings (SCORM 2004 has 64KB limit)
|
|
457
|
+
if (compressed.length > 64000) {
|
|
458
|
+
logger.error(`[Scorm2004Driver] ⚠️ CRITICAL: suspend_data is ${compressedSizeKB}KB compressed (over 64KB). Many LMSs will reject this!`);
|
|
459
|
+
eventBus.emit('suspend-data:critical', { bytes: compressed.length });
|
|
460
|
+
} else if (compressed.length > 32000) {
|
|
461
|
+
logger.warn(`[Scorm2004Driver] ⚠️ WARNING: suspend_data is ${compressedSizeKB}KB compressed (over 32KB). Approaching critical threshold.`);
|
|
462
|
+
eventBus.emit('suspend-data:warning', { bytes: compressed.length });
|
|
463
|
+
} else if (compressed.length > 4096) {
|
|
464
|
+
logger.info(`[Scorm2004Driver] ℹ️ INFO: suspend_data is ${compressedSizeKB}KB compressed (over 4KB).`);
|
|
465
|
+
} else {
|
|
466
|
+
logger.debug(`[Scorm2004Driver] ✓ suspend_data size: ${compressedSizeKB}KB compressed (healthy)`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this._setValue('cmi.suspend_data', compressed);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// Private: CMI Low-Level Access
|
|
475
|
+
// ============================================================================
|
|
476
|
+
|
|
477
|
+
_getValue(key) {
|
|
478
|
+
this._ensureInitialized();
|
|
479
|
+
|
|
480
|
+
if (this._isRecovered) {
|
|
481
|
+
return this._getValueRecovered(key);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const value = this._scorm.get(key);
|
|
485
|
+
const error = this._getScormError();
|
|
486
|
+
|
|
487
|
+
if (error) {
|
|
488
|
+
logger.error(`[Scorm2004Driver] GetValue('${key}') failed. Raw value: "${value}". Error:`, error);
|
|
489
|
+
this._throwScormError('GetValue', `Failed to get value for "${key}"`, error, { key });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (key === 'cmi.suspend_data') {
|
|
493
|
+
logger.debug('[Scorm2004Driver] getValue(\'cmi.suspend_data\') called');
|
|
494
|
+
logger.debug(`[Scorm2004Driver] Length: ${value ? value.length : 0}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return value || '';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
_getValueOptional(key) {
|
|
501
|
+
this._ensureInitialized();
|
|
502
|
+
|
|
503
|
+
if (this._isRecovered) {
|
|
504
|
+
return this._getValueOptionalRecovered(key);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const value = this._scorm.get(key);
|
|
508
|
+
const errorCode = this._scorm.debug.getCode();
|
|
509
|
+
|
|
510
|
+
// Error 403 = "Data Model Element Value Not Initialized" - expected for optional fields
|
|
511
|
+
if (errorCode === 403) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (errorCode !== 0) {
|
|
516
|
+
const error = {
|
|
517
|
+
code: errorCode,
|
|
518
|
+
message: this._scorm.debug.getInfo(errorCode)
|
|
519
|
+
};
|
|
520
|
+
logger.error(`[Scorm2004Driver] GetValue('${key}') failed. Raw value: "${value}". Error:`, error);
|
|
521
|
+
this._throwScormError('GetValue', `Failed to get value for "${key}"`, error, { key });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return value || null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_setValue(key, value) {
|
|
528
|
+
this._ensureInitialized();
|
|
529
|
+
|
|
530
|
+
if (this._isTerminated) {
|
|
531
|
+
if (import.meta.env.DEV) {
|
|
532
|
+
logger.warn(`[Scorm2004Driver] Ignoring setValue('${key}') - SCORM session already terminated`);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const stringValue = typeof value === 'string' ? value : String(value);
|
|
538
|
+
|
|
539
|
+
if (this._isRecovered) {
|
|
540
|
+
this._setValueRecovered(key, stringValue);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const success = this._scorm.set(key, stringValue);
|
|
545
|
+
|
|
546
|
+
if (!success) {
|
|
547
|
+
logger.error(`[Scorm2004Driver] SetValue('${key}') failed. Value length: ${stringValue.length}`);
|
|
548
|
+
const error = this._getScormError();
|
|
549
|
+
this._throwScormError('SetValue', `Failed to set value for "${key}"`, error, {
|
|
550
|
+
key,
|
|
551
|
+
valuePreview: stringValue.substring(0, 50)
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ============================================================================
|
|
557
|
+
// Private: Cache Population
|
|
558
|
+
// ============================================================================
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Populates the CMI cache at init time. Single LMS read pass.
|
|
562
|
+
*/
|
|
563
|
+
_populateCache() {
|
|
564
|
+
// Read-only scalars
|
|
565
|
+
this._cmiCache.entry = this._getValue('cmi.entry') || '';
|
|
566
|
+
this._cmiCache.bookmark = this._getValue('cmi.location') || '';
|
|
567
|
+
this._cmiCache.completionStatus = this._getValue('cmi.completion_status') || 'unknown';
|
|
568
|
+
this._cmiCache.successStatus = this._getValue('cmi.success_status') || 'unknown';
|
|
569
|
+
this._cmiCache.learnerId = this._getValue('cmi.learner_id') || '';
|
|
570
|
+
this._cmiCache.learnerName = this._getValue('cmi.learner_name') || '';
|
|
571
|
+
|
|
572
|
+
// Skip array hydration for fresh sessions
|
|
573
|
+
if (this._cmiCache.entry === 'ab-initio') {
|
|
574
|
+
logger.debug('[Scorm2004Driver] Fresh session — cache initialized empty');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Load objectives from CMI arrays
|
|
579
|
+
let objectivesCount = 0;
|
|
580
|
+
try {
|
|
581
|
+
objectivesCount = parseInt(this._getValue('cmi.objectives._count') || '0', 10);
|
|
582
|
+
} catch (_e) { /* No objectives stored — normal */ }
|
|
583
|
+
|
|
584
|
+
for (let i = 0; i < objectivesCount; i++) {
|
|
585
|
+
let id;
|
|
586
|
+
try {
|
|
587
|
+
id = this._getValue(`cmi.objectives.${i}.id`);
|
|
588
|
+
} catch (_e) { continue; }
|
|
589
|
+
if (!id) continue;
|
|
590
|
+
|
|
591
|
+
this._cmiCache.objectiveIdToIndex.set(id, i);
|
|
592
|
+
|
|
593
|
+
const success_status = this._getValueOptional(`cmi.objectives.${i}.success_status`) || 'unknown';
|
|
594
|
+
const completion_status = this._getValueOptional(`cmi.objectives.${i}.completion_status`) || 'incomplete';
|
|
595
|
+
|
|
596
|
+
this._cmiCache.objectives[id] = {
|
|
597
|
+
id,
|
|
598
|
+
success_status,
|
|
599
|
+
completion_status,
|
|
600
|
+
score: null,
|
|
601
|
+
progress_measure: null
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const scoreRaw = this._getValueOptional(`cmi.objectives.${i}.score.raw`);
|
|
605
|
+
if (scoreRaw) {
|
|
606
|
+
const parsed = parseFloat(scoreRaw);
|
|
607
|
+
if (!isNaN(parsed)) {
|
|
608
|
+
this._cmiCache.objectives[id].score = parsed;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const progressMeasure = this._getValueOptional(`cmi.objectives.${i}.progress_measure`);
|
|
613
|
+
if (progressMeasure) {
|
|
614
|
+
const parsed = parseFloat(progressMeasure);
|
|
615
|
+
if (!isNaN(parsed)) {
|
|
616
|
+
this._cmiCache.objectives[id].progress_measure = parsed;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const description = this._getValueOptional(`cmi.objectives.${i}.description`);
|
|
621
|
+
if (description) {
|
|
622
|
+
this._cmiCache.objectives[id].description = description;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Load interactions from CMI arrays
|
|
627
|
+
let interactionsCount = 0;
|
|
628
|
+
try {
|
|
629
|
+
interactionsCount = parseInt(this._getValue('cmi.interactions._count') || '0', 10);
|
|
630
|
+
} catch (_e) { /* No interactions stored — normal */ }
|
|
631
|
+
this._cmiCache.interactionsCount = isNaN(interactionsCount) ? 0 : interactionsCount;
|
|
632
|
+
|
|
633
|
+
for (let i = 0; i < interactionsCount; i++) {
|
|
634
|
+
const interaction = { _index: i };
|
|
635
|
+
|
|
636
|
+
try { interaction.id = this._getValue(`cmi.interactions.${i}.id`) || ''; } catch (_e) { interaction.id = ''; }
|
|
637
|
+
try { interaction.type = this._getValue(`cmi.interactions.${i}.type`) || ''; } catch (_e) { interaction.type = ''; }
|
|
638
|
+
|
|
639
|
+
interaction.learner_response = this._getValueOptional(`cmi.interactions.${i}.learner_response`) || '';
|
|
640
|
+
interaction.result = this._getValueOptional(`cmi.interactions.${i}.result`) || 'neutral';
|
|
641
|
+
interaction.timestamp = this._getValueOptional(`cmi.interactions.${i}.timestamp`) || '';
|
|
642
|
+
interaction.description = this._getValueOptional(`cmi.interactions.${i}.description`) || '';
|
|
643
|
+
|
|
644
|
+
const weighting = this._getValueOptional(`cmi.interactions.${i}.weighting`);
|
|
645
|
+
if (weighting) {
|
|
646
|
+
const parsed = parseFloat(weighting);
|
|
647
|
+
if (!isNaN(parsed)) {
|
|
648
|
+
interaction.weighting = parsed;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const latency = this._getValueOptional(`cmi.interactions.${i}.latency`);
|
|
653
|
+
if (latency) interaction.latency = latency;
|
|
654
|
+
|
|
655
|
+
this._cmiCache.interactions.push(interaction);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
logger.debug(`[Scorm2004Driver] Cache populated: ${objectivesCount} objective(s), ${interactionsCount} interaction(s)`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Gets or creates a CMI objective index for the given ID.
|
|
663
|
+
*/
|
|
664
|
+
_getOrCreateObjectiveIndex(objectiveId) {
|
|
665
|
+
if (this._cmiCache.objectiveIdToIndex.has(objectiveId)) {
|
|
666
|
+
return this._cmiCache.objectiveIdToIndex.get(objectiveId);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const newIndex = this._cmiCache.objectiveIdToIndex.size;
|
|
670
|
+
this._setValue(`cmi.objectives.${newIndex}.id`, objectiveId);
|
|
671
|
+
this._cmiCache.objectiveIdToIndex.set(objectiveId, newIndex);
|
|
672
|
+
|
|
673
|
+
return newIndex;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// Private Helpers
|
|
678
|
+
// ============================================================================
|
|
679
|
+
|
|
680
|
+
_getScormError() {
|
|
681
|
+
const code = this._scorm.debug.getCode();
|
|
682
|
+
|
|
683
|
+
if (code !== 0) {
|
|
684
|
+
logger.warn(`[Scorm2004Driver] getCode() returned: ${code}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (code === 0) return null;
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
code,
|
|
691
|
+
message: this._scorm.debug.getInfo(code)
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_throwScormError(operation, message, error, context = {}) {
|
|
696
|
+
const msg = error ? `${message}: ${error.message}` : message;
|
|
697
|
+
logger.error(msg, { domain: 'scorm', operation, ...context, error });
|
|
698
|
+
throw new Error(msg);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// --- Recovery Mode Helpers ---
|
|
702
|
+
|
|
703
|
+
_getValueRecovered(key) {
|
|
704
|
+
try {
|
|
705
|
+
const api = this._scorm.API.getHandle();
|
|
706
|
+
if (!api) throw new Error('SCORM API handle lost during recovery');
|
|
707
|
+
|
|
708
|
+
const value = api.GetValue(key);
|
|
709
|
+
const errCode = api.GetLastError ? parseInt(api.GetLastError(), 10) : 0;
|
|
710
|
+
|
|
711
|
+
if (errCode !== 0) {
|
|
712
|
+
throw new Error(`SCORM Error ${errCode}`);
|
|
713
|
+
}
|
|
714
|
+
return value || '';
|
|
715
|
+
} catch (e) {
|
|
716
|
+
this._throwScormError('GetValue (Recovered)', `Failed to get value for "${key}"`, e, { key });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
_getValueOptionalRecovered(key) {
|
|
721
|
+
try {
|
|
722
|
+
const api = this._scorm.API.getHandle();
|
|
723
|
+
if (!api) throw new Error('SCORM API handle lost during recovery');
|
|
724
|
+
|
|
725
|
+
const value = api.GetValue(key);
|
|
726
|
+
const errCode = api.GetLastError ? parseInt(api.GetLastError(), 10) : 0;
|
|
727
|
+
|
|
728
|
+
if (errCode === 403) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (errCode !== 0) {
|
|
733
|
+
throw new Error(`SCORM Error ${errCode}`);
|
|
734
|
+
}
|
|
735
|
+
return value || null;
|
|
736
|
+
} catch (e) {
|
|
737
|
+
this._throwScormError('GetValue (Recovered)', `Failed to get value for "${key}"`, e, { key });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
_setValueRecovered(key, value) {
|
|
742
|
+
try {
|
|
743
|
+
const api = this._scorm.API.getHandle();
|
|
744
|
+
if (!api) throw new Error('SCORM API handle lost during recovery');
|
|
745
|
+
|
|
746
|
+
const result = api.SetValue(key, value);
|
|
747
|
+
const success = result === 'true' || result === true;
|
|
748
|
+
|
|
749
|
+
if (!success) {
|
|
750
|
+
const errCode = api.GetLastError ? parseInt(api.GetLastError(), 10) : 0;
|
|
751
|
+
throw new Error(`SCORM Error ${errCode}`);
|
|
752
|
+
}
|
|
753
|
+
} catch (e) {
|
|
754
|
+
this._throwScormError('SetValue (Recovered)', `Failed to set value for "${key}"`, e, { key });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
_commitRecovered() {
|
|
759
|
+
try {
|
|
760
|
+
const api = this._scorm.API.getHandle();
|
|
761
|
+
if (!api) throw new Error('SCORM API handle lost during recovery');
|
|
762
|
+
|
|
763
|
+
const result = api.Commit('');
|
|
764
|
+
const success = result === 'true' || result === true;
|
|
765
|
+
|
|
766
|
+
if (!success) {
|
|
767
|
+
const errCode = api.GetLastError ? parseInt(api.GetLastError(), 10) : 0;
|
|
768
|
+
throw new Error(`SCORM Error ${errCode}`);
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
} catch (e) {
|
|
772
|
+
this._throwScormError('Commit (Recovered)', 'Failed to commit', e);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|