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,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file state-manager.js
|
|
3
|
+
* @description Sole public API for all state and LMS operations.
|
|
4
|
+
*
|
|
5
|
+
* This is the ONLY module that callers should import for state or LMS access.
|
|
6
|
+
* Internally composes focused modules:
|
|
7
|
+
* - DomainStore: domain CRUD with append-only semantics
|
|
8
|
+
* - CommitScheduler: auto-batched commit lifecycle
|
|
9
|
+
* - StateValidator: hydration, migration, validation
|
|
10
|
+
* - TransactionLog: ring buffer for debugging
|
|
11
|
+
*
|
|
12
|
+
* LMS communication flows through lmsConnection (internal, not exported).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { eventBus } from '../core/event-bus.js';
|
|
16
|
+
import lmsConnection from './lms-connection.js';
|
|
17
|
+
import xapiStatementService from './xapi-statement-service.js';
|
|
18
|
+
import { logger } from '../utilities/logger.js';
|
|
19
|
+
|
|
20
|
+
import { TransactionLog } from './transaction-log.js';
|
|
21
|
+
import { StateValidator } from './state-validation.js';
|
|
22
|
+
import { DomainStore } from './state-domains.js';
|
|
23
|
+
import { CommitScheduler } from './state-commits.js';
|
|
24
|
+
|
|
25
|
+
const VALID_COMPLETION_STATUSES = new Set(['completed', 'incomplete', 'not attempted', 'unknown']);
|
|
26
|
+
const VALID_SUCCESS_STATUSES = new Set(['passed', 'failed', 'unknown']);
|
|
27
|
+
const ISO_8601_DURATION_PATTERN = /^P(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formats milliseconds as ISO 8601 duration for SCORM 2004.
|
|
31
|
+
* @param {number} milliseconds
|
|
32
|
+
* @returns {string} ISO 8601 duration string
|
|
33
|
+
*/
|
|
34
|
+
function formatISO8601Duration(milliseconds) {
|
|
35
|
+
if (!milliseconds || milliseconds < 0) return 'PT0S';
|
|
36
|
+
|
|
37
|
+
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
38
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
39
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
40
|
+
const seconds = totalSeconds % 60;
|
|
41
|
+
|
|
42
|
+
let duration = 'PT';
|
|
43
|
+
if (hours > 0) duration += `${hours}H`;
|
|
44
|
+
if (minutes > 0) duration += `${minutes}M`;
|
|
45
|
+
if (seconds > 0 || (hours === 0 && minutes === 0)) duration += `${seconds}S`;
|
|
46
|
+
return duration;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class StateManager {
|
|
50
|
+
constructor() {
|
|
51
|
+
this.isInitialized = false;
|
|
52
|
+
this.isTerminated = false;
|
|
53
|
+
|
|
54
|
+
// Compose internal modules
|
|
55
|
+
this._txLog = new TransactionLog();
|
|
56
|
+
this._validator = new StateValidator();
|
|
57
|
+
this._domains = new DomainStore(this._txLog);
|
|
58
|
+
this._commits = new CommitScheduler(lmsConnection, this._domains, this._txLog);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =================================================================
|
|
62
|
+
// Connection Lifecycle (absorbed from main.js)
|
|
63
|
+
// =================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initializes the LMS connection, lifecycle handlers, and xAPI service.
|
|
67
|
+
* Call this BEFORE initialize().
|
|
68
|
+
* @returns {Promise<boolean>} True if connected
|
|
69
|
+
*/
|
|
70
|
+
async initializeConnection() {
|
|
71
|
+
const connected = await lmsConnection.initialize();
|
|
72
|
+
if (!connected) {
|
|
73
|
+
throw new Error('LMS initialization failed. Cannot start course without LMS connection.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lmsConnection.setupLifecycleHandlers();
|
|
77
|
+
|
|
78
|
+
// Initialize xAPI service — no-op for SCORM drivers (they don't implement xAPI methods)
|
|
79
|
+
xapiStatementService.initialize(lmsConnection.getDriver());
|
|
80
|
+
|
|
81
|
+
logger.debug('[StateManager] LMS connection established');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// =================================================================
|
|
86
|
+
// Validation Config
|
|
87
|
+
// =================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sets the course configuration used for state validation.
|
|
91
|
+
* Must be called BEFORE initialize().
|
|
92
|
+
*/
|
|
93
|
+
setCourseValidationConfig(config) {
|
|
94
|
+
if (this.isInitialized) {
|
|
95
|
+
throw new Error('StateManager: setCourseValidationConfig() must be called before initialize()');
|
|
96
|
+
}
|
|
97
|
+
this._validator.setCourseValidationConfig(config);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =================================================================
|
|
101
|
+
// State Initialization
|
|
102
|
+
// =================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initializes the StateManager and hydrates state from LMS.
|
|
106
|
+
* Call initializeConnection() first.
|
|
107
|
+
*/
|
|
108
|
+
initialize() {
|
|
109
|
+
if (this.isInitialized) {
|
|
110
|
+
throw new Error('StateManager: Already initialized. Do not call initialize() more than once.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this._checkCapabilities();
|
|
114
|
+
|
|
115
|
+
this._domains.state = this._validator.hydrateStateFromLMS(lmsConnection);
|
|
116
|
+
this.isInitialized = true;
|
|
117
|
+
|
|
118
|
+
logger.debug('[StateManager] Initialized and state hydrated from LMS');
|
|
119
|
+
eventBus.emit('state:initialized', this.getState());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// =================================================================
|
|
123
|
+
// Domain State CRUD (delegates to DomainStore)
|
|
124
|
+
// =================================================================
|
|
125
|
+
|
|
126
|
+
/** @returns {object} Deep-cloned copy of the entire state */
|
|
127
|
+
getState() {
|
|
128
|
+
this._assertInitialized();
|
|
129
|
+
return this._domains.getState();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @returns {any} Deep-cloned domain state, or undefined */
|
|
133
|
+
getDomainState(domain) {
|
|
134
|
+
this._assertInitialized();
|
|
135
|
+
return this._domains.getDomainState(domain);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Sets domain state and schedules a commit.
|
|
140
|
+
* Append-only domains (interactions) append rather than replace.
|
|
141
|
+
*/
|
|
142
|
+
setDomainState(domain, value, meta = {}) {
|
|
143
|
+
if (this.isTerminated) {
|
|
144
|
+
if (import.meta.env.DEV) {
|
|
145
|
+
logger.warn(`[StateManager] Ignoring setDomainState('${domain}') - session already terminated`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this._assertInitialized();
|
|
150
|
+
|
|
151
|
+
const result = this._domains.setDomainState(domain, value, meta);
|
|
152
|
+
|
|
153
|
+
// Report to driver for format-specific handling
|
|
154
|
+
if (domain === 'objectives' && value) {
|
|
155
|
+
this._reportObjectivesToDriver(value);
|
|
156
|
+
} else if (domain === 'interactions' && value) {
|
|
157
|
+
lmsConnection.reportInteraction(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Auto-batch: schedule debounced commit
|
|
161
|
+
this._commits.scheduleCommit(true);
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clears all suspend data, resetting the course.
|
|
168
|
+
* WARNING: Irreversible — deletes all learner progress.
|
|
169
|
+
*/
|
|
170
|
+
async clearAllData() {
|
|
171
|
+
this._assertInitialized();
|
|
172
|
+
this._assertNotTerminated('Cannot clear data after termination.');
|
|
173
|
+
|
|
174
|
+
logger.debug('[StateManager] Clearing all suspend data for course restart');
|
|
175
|
+
this._domains.clearState();
|
|
176
|
+
await this._commits.commitToLMS();
|
|
177
|
+
eventBus.emit('state:cleared', { reason: 'course restart' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sets LMS compatibility mode (auto/balanced/profiled behaviors).
|
|
182
|
+
* Must be called before initializeConnection() for deterministic startup.
|
|
183
|
+
*/
|
|
184
|
+
setCompatibilityMode(mode = 'auto') {
|
|
185
|
+
if (this.isInitialized) {
|
|
186
|
+
throw new Error('StateManager: setCompatibilityMode() must be called before initialize()');
|
|
187
|
+
}
|
|
188
|
+
lmsConnection.setCompatibilityMode(mode);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// =================================================================
|
|
192
|
+
// Semantic LMS Passthroughs
|
|
193
|
+
// =================================================================
|
|
194
|
+
|
|
195
|
+
/** @returns {string} The entry mode (ab-initio, resume, etc.) */
|
|
196
|
+
getEntryMode() {
|
|
197
|
+
this._assertInitialized();
|
|
198
|
+
return lmsConnection.getEntryMode();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @returns {string|null} The current bookmark (slide ID) */
|
|
202
|
+
getBookmark() {
|
|
203
|
+
return lmsConnection.getBookmark();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @param {string} slideId */
|
|
207
|
+
setBookmark(slideId) {
|
|
208
|
+
if (typeof slideId !== 'string' || !slideId.trim()) {
|
|
209
|
+
throw new Error('StateManager: bookmark must be a non-empty string');
|
|
210
|
+
}
|
|
211
|
+
if (slideId.length > 1024) {
|
|
212
|
+
throw new Error('StateManager: bookmark exceeds maximum length (1024)');
|
|
213
|
+
}
|
|
214
|
+
lmsConnection.setBookmark(slideId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @param {{raw: number, scaled: number, min: number, max: number}} score */
|
|
218
|
+
reportScore(score) {
|
|
219
|
+
this._assertValidScore(score);
|
|
220
|
+
lmsConnection.reportScore(score);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** @param {string} status - 'completed' | 'incomplete' */
|
|
224
|
+
reportCompletion(status) {
|
|
225
|
+
this._assertValidCompletionStatus(status);
|
|
226
|
+
lmsConnection.reportCompletion(status);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @param {string} status - 'passed' | 'failed' | 'unknown' */
|
|
230
|
+
reportSuccess(status) {
|
|
231
|
+
this._assertValidSuccessStatus(status);
|
|
232
|
+
lmsConnection.reportSuccess(status);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @returns {string} Current completion status */
|
|
236
|
+
getCompletion() {
|
|
237
|
+
return lmsConnection.getCompletion();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @returns {string} Current success status */
|
|
241
|
+
getSuccess() {
|
|
242
|
+
return lmsConnection.getSuccess();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getScore() {
|
|
246
|
+
return lmsConnection.getScore();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @returns {string} LMS format (scorm2004, scorm1.2, cmi5, lti) */
|
|
250
|
+
getFormat() {
|
|
251
|
+
return lmsConnection.getFormat();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gets cmi5 launch data (moveOn, masteryScore, etc.).
|
|
256
|
+
* @returns {Object|null} Launch data or null if not available
|
|
257
|
+
*/
|
|
258
|
+
getLaunchData() {
|
|
259
|
+
return lmsConnection.getLaunchData();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @returns {Object} Driver capabilities */
|
|
263
|
+
getCapabilities() {
|
|
264
|
+
return lmsConnection.getCapabilities();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
getLmsDiagnostics() {
|
|
268
|
+
return lmsConnection.getDiagnostics();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =================================================================
|
|
272
|
+
// Session Lifecycle
|
|
273
|
+
// =================================================================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Exits with 'suspend' status (user intends to return).
|
|
277
|
+
*/
|
|
278
|
+
async exitCourseWithSuspend() {
|
|
279
|
+
this._assertInitialized();
|
|
280
|
+
this._assertNotTerminated('Cannot exit course again.');
|
|
281
|
+
|
|
282
|
+
await this._commits.flush();
|
|
283
|
+
this._reportSessionTime();
|
|
284
|
+
|
|
285
|
+
logger.debug('[StateManager] Exiting course with suspend status');
|
|
286
|
+
lmsConnection.setExitMode('suspend');
|
|
287
|
+
return await this.terminate();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Exits with 'normal' status (course is finished).
|
|
292
|
+
*/
|
|
293
|
+
async exitCourseComplete() {
|
|
294
|
+
this._assertInitialized();
|
|
295
|
+
this._assertNotTerminated('Cannot exit course again.');
|
|
296
|
+
|
|
297
|
+
await this._commits.flush();
|
|
298
|
+
this._reportSessionTime();
|
|
299
|
+
|
|
300
|
+
logger.debug('[StateManager] Exiting course with normal status');
|
|
301
|
+
lmsConnection.setExitMode('normal');
|
|
302
|
+
return await this.terminate();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Terminates the LMS connection with a final commit.
|
|
307
|
+
*/
|
|
308
|
+
async terminate() {
|
|
309
|
+
this._assertInitialized();
|
|
310
|
+
this._assertNotTerminated('Cannot terminate again.');
|
|
311
|
+
|
|
312
|
+
logger.debug('[StateManager] Terminating...');
|
|
313
|
+
|
|
314
|
+
// Emit BEFORE termination so services can send final xAPI statements
|
|
315
|
+
await eventBus.emitAsync('session:beforeTerminate');
|
|
316
|
+
|
|
317
|
+
await this._commits.commitToLMS();
|
|
318
|
+
this.isTerminated = true;
|
|
319
|
+
|
|
320
|
+
return await lmsConnection.terminate();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// =================================================================
|
|
324
|
+
// Progress
|
|
325
|
+
// =================================================================
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Updates progress_measure based on visited slides.
|
|
329
|
+
* @param {number} totalSlides - Total sequential slides
|
|
330
|
+
*/
|
|
331
|
+
updateProgressMeasure(totalSlides) {
|
|
332
|
+
this._assertInitialized();
|
|
333
|
+
this._assertNotTerminated('Cannot update progress after termination.');
|
|
334
|
+
if (!totalSlides || totalSlides <= 0) {
|
|
335
|
+
throw new Error('StateManager: totalSlides must be provided and greater than 0');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const navigationState = this.getDomainState('navigation') || {};
|
|
340
|
+
const visitedSlides = navigationState.visitedSlides || [];
|
|
341
|
+
const visitedCount = visitedSlides.length;
|
|
342
|
+
|
|
343
|
+
let progressMeasure = Math.min(visitedCount / totalSlides, 1.0);
|
|
344
|
+
progressMeasure = Math.max(0, Math.min(1, progressMeasure));
|
|
345
|
+
progressMeasure = Math.round(progressMeasure * 100) / 100;
|
|
346
|
+
|
|
347
|
+
lmsConnection.reportProgress(progressMeasure);
|
|
348
|
+
|
|
349
|
+
logger.debug(`[StateManager] Progress measure updated: ${progressMeasure} (${(progressMeasure * 100).toFixed(0)}%)`);
|
|
350
|
+
logger.debug(` - Slides visited: ${visitedCount}/${totalSlides}`);
|
|
351
|
+
|
|
352
|
+
eventBus.emit('progress:updated', { progressMeasure, visitedSlides: visitedCount, totalSlides });
|
|
353
|
+
return progressMeasure;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logger.error('[StateManager] Error updating progress_measure:', { domain: 'state', operation: 'updateProgressMeasure', stack: error.stack });
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// =================================================================
|
|
361
|
+
// Commit Control
|
|
362
|
+
// =================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Immediately flushes any pending auto-batched writes.
|
|
366
|
+
*/
|
|
367
|
+
async flush() {
|
|
368
|
+
this._assertInitialized();
|
|
369
|
+
await this._commits.flush();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// =================================================================
|
|
373
|
+
// Debugging
|
|
374
|
+
// =================================================================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Gets recent state transaction log entries.
|
|
378
|
+
* @param {number} [n=10]
|
|
379
|
+
*/
|
|
380
|
+
getTransactionLog(n = 10) {
|
|
381
|
+
return this._txLog.getRecent(n);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// =================================================================
|
|
385
|
+
// Private
|
|
386
|
+
// =================================================================
|
|
387
|
+
|
|
388
|
+
_assertInitialized() {
|
|
389
|
+
if (!this.isInitialized) {
|
|
390
|
+
throw new Error('StateManager: Not initialized. Call initialize() first.');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_assertNotTerminated(msg) {
|
|
395
|
+
if (this.isTerminated) {
|
|
396
|
+
throw new Error(`StateManager: Already terminated. ${msg}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_checkCapabilities() {
|
|
401
|
+
try {
|
|
402
|
+
const caps = lmsConnection.getCapabilities();
|
|
403
|
+
const format = lmsConnection.getFormat();
|
|
404
|
+
|
|
405
|
+
if (!caps.supportsObjectives) {
|
|
406
|
+
logger.warn(`[StateManager] ${format} driver does not support objectives — they will be stored in suspend_data only`);
|
|
407
|
+
}
|
|
408
|
+
if (!caps.supportsInteractions) {
|
|
409
|
+
logger.warn(`[StateManager] ${format} driver does not support interactions — they will be stored in suspend_data only`);
|
|
410
|
+
}
|
|
411
|
+
if (caps.maxSuspendDataBytes > 0) {
|
|
412
|
+
logger.debug(`[StateManager] Suspend data limit: ${caps.maxSuspendDataBytes} bytes (${format})`);
|
|
413
|
+
}
|
|
414
|
+
eventBus.emit('state:capabilities', caps);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.debug('[StateManager] Could not check driver capabilities:', error.message);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_reportObjectivesToDriver(objectives) {
|
|
421
|
+
if (!objectives || typeof objectives !== 'object') return;
|
|
422
|
+
for (const [id, objective] of Object.entries(objectives)) {
|
|
423
|
+
this._assertValidObjective({ id, ...objective });
|
|
424
|
+
lmsConnection.reportObjective({ id, ...objective });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_reportSessionTime() {
|
|
429
|
+
try {
|
|
430
|
+
const sessionStartTime = lmsConnection.sessionStart;
|
|
431
|
+
if (!sessionStartTime) {
|
|
432
|
+
logger.warn('[StateManager] No session start time available, cannot report session_time');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const elapsedMs = Date.now() - sessionStartTime;
|
|
436
|
+
const duration = formatISO8601Duration(elapsedMs);
|
|
437
|
+
if (!ISO_8601_DURATION_PATTERN.test(duration)) {
|
|
438
|
+
throw new Error(`Invalid ISO 8601 session duration generated: ${duration}`);
|
|
439
|
+
}
|
|
440
|
+
lmsConnection.reportSessionTime(duration);
|
|
441
|
+
logger.debug(`[StateManager] Session time reported: ${duration} (${Math.round(elapsedMs / 1000)}s)`);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
logger.warn('[StateManager] Failed to report session_time:', error.message);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
_assertValidScore(score) {
|
|
448
|
+
if (!score || typeof score !== 'object') {
|
|
449
|
+
throw new Error('StateManager: score must be an object');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const { raw, scaled, min, max } = score;
|
|
453
|
+
if (raw !== undefined && (!Number.isFinite(raw) || raw < 0 || raw > 100)) {
|
|
454
|
+
throw new Error(`StateManager: score.raw must be between 0 and 100, got ${raw}`);
|
|
455
|
+
}
|
|
456
|
+
if (scaled !== undefined && (!Number.isFinite(scaled) || scaled < 0 || scaled > 1)) {
|
|
457
|
+
throw new Error(`StateManager: score.scaled must be between 0 and 1, got ${scaled}`);
|
|
458
|
+
}
|
|
459
|
+
if (min !== undefined && !Number.isFinite(min)) {
|
|
460
|
+
throw new Error(`StateManager: score.min must be numeric, got ${min}`);
|
|
461
|
+
}
|
|
462
|
+
if (max !== undefined && !Number.isFinite(max)) {
|
|
463
|
+
throw new Error(`StateManager: score.max must be numeric, got ${max}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_assertValidCompletionStatus(status) {
|
|
468
|
+
if (!VALID_COMPLETION_STATUSES.has(status)) {
|
|
469
|
+
throw new Error(`StateManager: invalid completion status "${status}"`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_assertValidSuccessStatus(status) {
|
|
474
|
+
if (!VALID_SUCCESS_STATUSES.has(status)) {
|
|
475
|
+
throw new Error(`StateManager: invalid success status "${status}"`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_assertValidObjective(objective) {
|
|
480
|
+
if (!objective || typeof objective.id !== 'string' || !objective.id.trim()) {
|
|
481
|
+
throw new Error('StateManager: objective.id is required');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (objective.score !== undefined && objective.score !== null) {
|
|
485
|
+
if (!Number.isFinite(objective.score) || objective.score < 0 || objective.score > 100) {
|
|
486
|
+
throw new Error(`StateManager: objective.score must be between 0 and 100, got ${objective.score}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (objective.progress_measure !== undefined && objective.progress_measure !== null) {
|
|
491
|
+
if (!Number.isFinite(objective.progress_measure) || objective.progress_measure < 0 || objective.progress_measure > 1) {
|
|
492
|
+
throw new Error(`StateManager: objective.progress_measure must be between 0 and 1, got ${objective.progress_measure}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const instance = new StateManager();
|
|
499
|
+
export default instance;
|
|
500
|
+
|
|
501
|
+
// Exported for unit testing
|
|
502
|
+
export { formatISO8601Duration };
|