scorm-again 2.0.0 → 2.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.
Files changed (77) hide show
  1. package/.github/workflows/stale.yml +14 -0
  2. package/.run/{Mocha Unit Tests.run.xml → Mocha Unit Tests (watch).run.xml } +1 -1
  3. package/.run/Template Mocha.run.xml +17 -0
  4. package/README.md +171 -72
  5. package/dist/aicc.js +1441 -1140
  6. package/dist/aicc.js.map +1 -1
  7. package/dist/aicc.min.js +1 -1
  8. package/dist/aicc.min.js.map +1 -1
  9. package/dist/scorm-again.js +2703 -2212
  10. package/dist/scorm-again.js.map +1 -1
  11. package/dist/scorm-again.min.js +1 -1
  12. package/dist/scorm-again.min.js.map +1 -1
  13. package/dist/scorm12.js +1069 -852
  14. package/dist/scorm12.js.map +1 -1
  15. package/dist/scorm12.min.js +1 -1
  16. package/dist/scorm12.min.js.map +1 -1
  17. package/dist/scorm2004.js +1861 -1571
  18. package/dist/scorm2004.js.map +1 -1
  19. package/dist/scorm2004.min.js +1 -1
  20. package/dist/scorm2004.min.js.map +1 -1
  21. package/package.json +10 -6
  22. package/src/AICC.ts +15 -17
  23. package/src/BaseAPI.ts +268 -417
  24. package/src/Scorm12API.ts +65 -38
  25. package/src/Scorm2004API.ts +151 -117
  26. package/src/cmi/aicc/attempts.ts +94 -0
  27. package/src/cmi/aicc/cmi.ts +100 -0
  28. package/src/cmi/aicc/core.ts +360 -0
  29. package/src/cmi/aicc/evaluation.ts +157 -0
  30. package/src/cmi/aicc/paths.ts +180 -0
  31. package/src/cmi/aicc/student_data.ts +86 -0
  32. package/src/cmi/aicc/student_demographics.ts +367 -0
  33. package/src/cmi/aicc/student_preferences.ts +176 -0
  34. package/src/cmi/aicc/tries.ts +116 -0
  35. package/src/cmi/aicc/validation.ts +25 -0
  36. package/src/cmi/common/array.ts +77 -0
  37. package/src/cmi/common/base_cmi.ts +46 -0
  38. package/src/cmi/common/score.ts +203 -0
  39. package/src/cmi/common/validation.ts +60 -0
  40. package/src/cmi/scorm12/cmi.ts +224 -0
  41. package/src/cmi/scorm12/interactions.ts +368 -0
  42. package/src/cmi/scorm12/nav.ts +54 -0
  43. package/src/cmi/scorm12/objectives.ts +112 -0
  44. package/src/cmi/scorm12/student_data.ts +130 -0
  45. package/src/cmi/scorm12/student_preference.ts +158 -0
  46. package/src/cmi/scorm12/validation.ts +48 -0
  47. package/src/cmi/scorm2004/adl.ts +272 -0
  48. package/src/cmi/scorm2004/cmi.ts +599 -0
  49. package/src/cmi/scorm2004/comments.ts +163 -0
  50. package/src/cmi/scorm2004/interactions.ts +466 -0
  51. package/src/cmi/scorm2004/learner_preference.ts +152 -0
  52. package/src/cmi/scorm2004/objectives.ts +212 -0
  53. package/src/cmi/scorm2004/score.ts +78 -0
  54. package/src/cmi/scorm2004/validation.ts +42 -0
  55. package/src/constants/default_settings.ts +81 -0
  56. package/src/constants/enums.ts +5 -0
  57. package/src/constants/regex.ts +2 -2
  58. package/src/constants/response_constants.ts +2 -0
  59. package/src/exceptions.ts +22 -1
  60. package/src/helpers/scheduled_commit.ts +42 -0
  61. package/src/interfaces/IBaseAPI.ts +35 -0
  62. package/src/types/api_types.ts +32 -0
  63. package/src/utilities/debounce.ts +31 -0
  64. package/src/utilities.ts +56 -0
  65. package/test/AICC.spec.ts +11 -1
  66. package/test/Scorm12API.spec.ts +262 -9
  67. package/test/Scorm2004API.spec.ts +488 -2
  68. package/test/cmi/aicc_cmi.spec.ts +188 -11
  69. package/test/cmi/scorm12_cmi.spec.ts +5 -5
  70. package/test/cmi/scorm2004_cmi.spec.ts +8 -8
  71. package/test/cmi_helpers.ts +1 -1
  72. package/test/types/api_types.spec.ts +126 -0
  73. package/test/utilities/debounce.spec.ts +56 -0
  74. package/src/cmi/aicc_cmi.ts +0 -1248
  75. package/src/cmi/common.ts +0 -411
  76. package/src/cmi/scorm12_cmi.ts +0 -1426
  77. package/src/cmi/scorm2004_cmi.ts +0 -1874
package/src/BaseAPI.ts CHANGED
@@ -1,154 +1,20 @@
1
- import { BaseCMI, CMIArray } from "./cmi/common";
1
+ import { CMIArray } from "./cmi/common/array";
2
2
  import { ValidationError } from "./exceptions";
3
3
  import ErrorCodes, { ErrorCode } from "./constants/error_codes";
4
4
  import APIConstants from "./constants/api_constants";
5
- import { unflatten } from "./utilities";
6
-
7
- const global_constants = APIConstants.global;
8
- const scorm12_error_codes = ErrorCodes.scorm12;
9
-
10
- /**
11
- * Debounce function to delay the execution of a given function.
12
- *
13
- * @param func - The function to debounce.
14
- * @param wait - The number of milliseconds to delay.
15
- * @param immediate - If `true`, the function will be triggered on the leading edge instead of the trailing.
16
- * @returns A debounced version of the provided function.
17
- */
18
- function debounce<T extends (...args: any[]) => void>(
19
- func: T,
20
- wait: number,
21
- immediate = false,
22
- ): (...args: Parameters<T>) => void {
23
- let timeout: ReturnType<typeof setTimeout> | null;
24
-
25
- return function (this: any, ...args: Parameters<T>) {
26
- const context = this;
27
-
28
- const later = () => {
29
- timeout = null;
30
- if (!immediate) func.apply(context, args);
31
- };
32
-
33
- const callNow = immediate && !timeout;
34
-
35
- if (timeout) clearTimeout(timeout);
36
- timeout = setTimeout(later, wait);
37
-
38
- if (callNow) func.apply(context, args);
39
- };
40
- }
41
-
42
- export type Settings = {
43
- autocommit: boolean;
44
- autocommitSeconds: number;
45
- asyncCommit: boolean;
46
- sendBeaconCommit: boolean;
47
- lmsCommitUrl: boolean | string;
48
- dataCommitFormat: string;
49
- commitRequestDataType: string;
50
- autoProgress: boolean;
51
- logLevel: number;
52
- selfReportSessionTime: boolean;
53
- alwaysSendTotalTime: boolean;
54
- strict_errors: boolean;
55
- xhrHeaders: RefObject;
56
- xhrWithCredentials: boolean;
57
- responseHandler: (response: Response) => Promise<ResultObject>;
58
- requestHandler: (commitObject: any) => any;
59
- onLogMessage: (messageLevel: number, logMessage: string) => void;
60
- mastery_override?: boolean;
61
- };
62
-
63
- export type RefObject = {
64
- [key: string]: any;
65
- };
66
-
67
- export type ResultObject = {
68
- result: string;
69
- errorCode: number;
70
- navRequest?: string;
71
- };
72
-
73
- export const DefaultSettings: Settings = {
74
- autocommit: false,
75
- autocommitSeconds: 10,
76
- asyncCommit: false,
77
- sendBeaconCommit: false,
78
- lmsCommitUrl: false,
79
- dataCommitFormat: "json",
80
- commitRequestDataType: "application/json;charset=UTF-8",
81
- autoProgress: false,
82
- logLevel: global_constants.LOG_LEVEL_ERROR,
83
- selfReportSessionTime: false,
84
- alwaysSendTotalTime: false,
85
- strict_errors: true,
86
- xhrHeaders: {},
87
- xhrWithCredentials: false,
88
- responseHandler: async function (response: Response): Promise<ResultObject> {
89
- if (typeof response !== "undefined") {
90
- const httpResult = JSON.parse(await response.text());
91
- if (
92
- httpResult === null ||
93
- !{}.hasOwnProperty.call(httpResult, "result")
94
- ) {
95
- if (response.status === 200) {
96
- return {
97
- result: global_constants.SCORM_TRUE,
98
- errorCode: 0,
99
- };
100
- } else {
101
- return {
102
- result: global_constants.SCORM_FALSE,
103
- errorCode: 101,
104
- };
105
- }
106
- } else {
107
- return {
108
- result: httpResult.result,
109
- errorCode: httpResult.errorCode
110
- ? httpResult.errorCode
111
- : httpResult.result === global_constants.SCORM_TRUE
112
- ? 0
113
- : 101,
114
- };
115
- }
116
- }
117
- return {
118
- result: global_constants.SCORM_FALSE,
119
- errorCode: 101,
120
- };
121
- },
122
- requestHandler: function (commitObject) {
123
- return commitObject;
124
- },
125
- onLogMessage: function (messageLevel, logMessage) {
126
- switch (messageLevel) {
127
- case global_constants.LOG_LEVEL_ERROR:
128
- console.error(logMessage);
129
- break;
130
- case global_constants.LOG_LEVEL_WARNING:
131
- console.warn(logMessage);
132
- break;
133
- case global_constants.LOG_LEVEL_INFO:
134
- console.info(logMessage);
135
- break;
136
- case global_constants.LOG_LEVEL_DEBUG:
137
- if (console.debug) {
138
- console.debug(logMessage);
139
- } else {
140
- console.log(logMessage);
141
- }
142
- break;
143
- }
144
- },
145
- };
5
+ import { formatMessage, stringMatches, unflatten } from "./utilities";
6
+ import { BaseCMI } from "./cmi/common/base_cmi";
7
+ import { debounce } from "./utilities/debounce";
8
+ import { RefObject, ResultObject, Settings } from "./types/api_types";
9
+ import { DefaultSettings } from "./constants/default_settings";
10
+ import { IBaseAPI } from "./interfaces/IBaseAPI";
11
+ import { ScheduledCommit } from "./helpers/scheduled_commit";
146
12
 
147
13
  /**
148
14
  * Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
149
15
  * abstract, and never initialized on its own.
150
16
  */
151
- export default abstract class BaseAPI {
17
+ export default abstract class BaseAPI implements IBaseAPI {
152
18
  private _timeout?: ScheduledCommit;
153
19
  private readonly _error_codes: ErrorCode;
154
20
  private _settings: Settings = DefaultSettings;
@@ -163,7 +29,7 @@ export default abstract class BaseAPI {
163
29
  if (new.target === BaseAPI) {
164
30
  throw new TypeError("Cannot construct BaseAPI instances directly");
165
31
  }
166
- this.currentState = global_constants.STATE_NOT_INITIALIZED;
32
+ this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
167
33
  this.lastErrorCode = "0";
168
34
  this.listenerArray = [];
169
35
 
@@ -185,6 +51,21 @@ export default abstract class BaseAPI {
185
51
  public apiLogLevel: number;
186
52
  public selfReportSessionTime: boolean;
187
53
 
54
+ abstract reset(settings?: Settings): void;
55
+
56
+ /**
57
+ * Common reset method for all APIs. New settings are merged with the existing settings.
58
+ * @param {Settings} settings
59
+ * @protected
60
+ */
61
+ commonReset(settings?: Settings): void {
62
+ this.settings = { ...this.settings, ...settings };
63
+
64
+ this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
65
+ this.lastErrorCode = "0";
66
+ this.listenerArray = [];
67
+ }
68
+
188
69
  /**
189
70
  * Initialize the API
190
71
  * @param {string} callbackName
@@ -197,7 +78,7 @@ export default abstract class BaseAPI {
197
78
  initializeMessage?: string,
198
79
  terminationMessage?: string,
199
80
  ): string {
200
- let returnValue = global_constants.SCORM_FALSE;
81
+ let returnValue = APIConstants.global.SCORM_FALSE;
201
82
 
202
83
  if (this.isInitialized()) {
203
84
  this.throwSCORMError(this._error_codes.INITIALIZED, initializeMessage);
@@ -208,16 +89,16 @@ export default abstract class BaseAPI {
208
89
  this.cmi.setStartTime();
209
90
  }
210
91
 
211
- this.currentState = global_constants.STATE_INITIALIZED;
92
+ this.currentState = APIConstants.global.STATE_INITIALIZED;
212
93
  this.lastErrorCode = "0";
213
- returnValue = global_constants.SCORM_TRUE;
94
+ returnValue = APIConstants.global.SCORM_TRUE;
214
95
  this.processListeners(callbackName);
215
96
  }
216
97
 
217
98
  this.apiLog(
218
99
  callbackName,
219
100
  "returned: " + returnValue,
220
- global_constants.LOG_LEVEL_INFO,
101
+ APIConstants.global.LOG_LEVEL_INFO,
221
102
  );
222
103
  this.clearSCORMError(returnValue);
223
104
 
@@ -240,6 +121,71 @@ export default abstract class BaseAPI {
240
121
 
241
122
  abstract lmsGetDiagnostic(CMIErrorCode: string | number): string;
242
123
 
124
+ /**
125
+ * Abstract method for validating that a response is correct.
126
+ *
127
+ * @param {string} _CMIElement
128
+ * @param {any} _value
129
+ */
130
+ abstract validateCorrectResponse(_CMIElement: string, _value: any): void;
131
+
132
+ /**
133
+ * Gets or builds a new child element to add to the array.
134
+ * APIs that inherit BaseAPI should override this method.
135
+ *
136
+ * @param {string} _CMIElement - unused
137
+ * @param {*} _value - unused
138
+ * @param {boolean} _foundFirstIndex - unused
139
+ * @return {BaseCMI|null}
140
+ * @abstract
141
+ */
142
+ abstract getChildElement(
143
+ _CMIElement: string,
144
+ _value: any,
145
+ _foundFirstIndex: boolean,
146
+ ): BaseCMI | null;
147
+
148
+ /**
149
+ * Attempts to store the data to the LMS, logs data if no LMS configured
150
+ * APIs that inherit BaseAPI should override this function
151
+ *
152
+ * @param {boolean} _calculateTotalTime
153
+ * @return {ResultObject}
154
+ * @abstract
155
+ */
156
+ abstract storeData(_calculateTotalTime: boolean): Promise<ResultObject>;
157
+
158
+ /**
159
+ * Render the cmi object to the proper format for LMS commit
160
+ * APIs that inherit BaseAPI should override this function
161
+ *
162
+ * @param {boolean} _terminateCommit
163
+ * @return {RefObject|Array}
164
+ * @abstract
165
+ */
166
+ abstract renderCommitCMI(_terminateCommit: boolean): RefObject | Array<any>;
167
+
168
+ /**
169
+ * Logging for all SCORM actions
170
+ *
171
+ * @param {string} functionName
172
+ * @param {string} logMessage
173
+ * @param {number} messageLevel
174
+ * @param {string} CMIElement
175
+ */
176
+ apiLog(
177
+ functionName: string,
178
+ logMessage: string,
179
+ messageLevel: number,
180
+ CMIElement?: string,
181
+ ) {
182
+ logMessage = formatMessage(functionName, logMessage, CMIElement);
183
+
184
+ if (messageLevel >= this.apiLogLevel) {
185
+ this.settings.onLogMessage(messageLevel, logMessage);
186
+ }
187
+ }
188
+
243
189
  /**
244
190
  * Getter for _error_codes
245
191
  * @return {ErrorCode}
@@ -270,8 +216,11 @@ export default abstract class BaseAPI {
270
216
  * @param {boolean} checkTerminated
271
217
  * @return {string}
272
218
  */
273
- terminate(callbackName: string, checkTerminated: boolean): string {
274
- let returnValue = global_constants.SCORM_FALSE;
219
+ async terminate(
220
+ callbackName: string,
221
+ checkTerminated: boolean,
222
+ ): Promise<string> {
223
+ let returnValue = APIConstants.global.SCORM_FALSE;
275
224
 
276
225
  if (
277
226
  this.checkState(
@@ -280,27 +229,27 @@ export default abstract class BaseAPI {
280
229
  this._error_codes.MULTIPLE_TERMINATION,
281
230
  )
282
231
  ) {
283
- this.currentState = global_constants.STATE_TERMINATED;
232
+ this.currentState = APIConstants.global.STATE_TERMINATED;
284
233
 
285
- const result: ResultObject = this.storeData(true);
234
+ const result: ResultObject = await this.storeData(true);
286
235
  if (typeof result.errorCode !== "undefined" && result.errorCode > 0) {
287
236
  this.throwSCORMError(result.errorCode);
288
237
  }
289
238
  returnValue =
290
239
  typeof result !== "undefined" && result.result
291
240
  ? result.result
292
- : global_constants.SCORM_FALSE;
241
+ : APIConstants.global.SCORM_FALSE;
293
242
 
294
243
  if (checkTerminated) this.lastErrorCode = "0";
295
244
 
296
- returnValue = global_constants.SCORM_TRUE;
245
+ returnValue = APIConstants.global.SCORM_TRUE;
297
246
  this.processListeners(callbackName);
298
247
  }
299
248
 
300
249
  this.apiLog(
301
250
  callbackName,
302
251
  "returned: " + returnValue,
303
- global_constants.LOG_LEVEL_INFO,
252
+ APIConstants.global.LOG_LEVEL_INFO,
304
253
  );
305
254
  this.clearSCORMError(returnValue);
306
255
 
@@ -341,9 +290,14 @@ export default abstract class BaseAPI {
341
290
  this.apiLog(
342
291
  callbackName,
343
292
  ": returned: " + returnValue,
344
- global_constants.LOG_LEVEL_INFO,
293
+ APIConstants.global.LOG_LEVEL_INFO,
345
294
  CMIElement,
346
295
  );
296
+
297
+ if (returnValue === undefined) {
298
+ return "";
299
+ }
300
+
347
301
  this.clearSCORMError(returnValue);
348
302
 
349
303
  return returnValue;
@@ -369,7 +323,7 @@ export default abstract class BaseAPI {
369
323
  if (value !== undefined) {
370
324
  value = String(value);
371
325
  }
372
- let returnValue: string = global_constants.SCORM_FALSE;
326
+ let returnValue: string = APIConstants.global.SCORM_FALSE;
373
327
 
374
328
  if (
375
329
  this.checkState(
@@ -388,7 +342,7 @@ export default abstract class BaseAPI {
388
342
  }
389
343
 
390
344
  if (returnValue === undefined) {
391
- returnValue = global_constants.SCORM_FALSE;
345
+ returnValue = APIConstants.global.SCORM_FALSE;
392
346
  }
393
347
 
394
348
  // If we didn't have any errors while setting the data, go ahead and
@@ -405,7 +359,7 @@ export default abstract class BaseAPI {
405
359
  this.apiLog(
406
360
  callbackName,
407
361
  ": " + value + ": result: " + returnValue,
408
- global_constants.LOG_LEVEL_INFO,
362
+ APIConstants.global.LOG_LEVEL_INFO,
409
363
  CMIElement,
410
364
  );
411
365
  this.clearSCORMError(returnValue);
@@ -419,10 +373,13 @@ export default abstract class BaseAPI {
419
373
  * @param {boolean} checkTerminated
420
374
  * @return {string}
421
375
  */
422
- commit(callbackName: string, checkTerminated: boolean = false): string {
376
+ async commit(
377
+ callbackName: string,
378
+ checkTerminated: boolean = false,
379
+ ): Promise<string> {
423
380
  this.clearScheduledCommit();
424
381
 
425
- let returnValue = global_constants.SCORM_FALSE;
382
+ let returnValue = APIConstants.global.SCORM_FALSE;
426
383
 
427
384
  if (
428
385
  this.checkState(
@@ -431,19 +388,19 @@ export default abstract class BaseAPI {
431
388
  this._error_codes.COMMIT_AFTER_TERM,
432
389
  )
433
390
  ) {
434
- const result = this.storeData(false);
391
+ const result = await this.storeData(false);
435
392
  if (result.errorCode && result.errorCode > 0) {
436
393
  this.throwSCORMError(result.errorCode);
437
394
  }
438
395
  returnValue =
439
396
  typeof result !== "undefined" && result.result
440
397
  ? result.result
441
- : global_constants.SCORM_FALSE;
398
+ : APIConstants.global.SCORM_FALSE;
442
399
 
443
400
  this.apiLog(
444
401
  callbackName,
445
402
  " Result: " + returnValue,
446
- global_constants.LOG_LEVEL_DEBUG,
403
+ APIConstants.global.LOG_LEVEL_DEBUG,
447
404
  "HttpRequest",
448
405
  );
449
406
 
@@ -455,7 +412,7 @@ export default abstract class BaseAPI {
455
412
  this.apiLog(
456
413
  callbackName,
457
414
  "returned: " + returnValue,
458
- global_constants.LOG_LEVEL_INFO,
415
+ APIConstants.global.LOG_LEVEL_INFO,
459
416
  );
460
417
  this.clearSCORMError(returnValue);
461
418
 
@@ -475,7 +432,7 @@ export default abstract class BaseAPI {
475
432
  this.apiLog(
476
433
  callbackName,
477
434
  "returned: " + returnValue,
478
- global_constants.LOG_LEVEL_INFO,
435
+ APIConstants.global.LOG_LEVEL_INFO,
479
436
  );
480
437
 
481
438
  return returnValue;
@@ -499,7 +456,7 @@ export default abstract class BaseAPI {
499
456
  this.apiLog(
500
457
  callbackName,
501
458
  "returned: " + returnValue,
502
- global_constants.LOG_LEVEL_INFO,
459
+ APIConstants.global.LOG_LEVEL_INFO,
503
460
  );
504
461
 
505
462
  return returnValue;
@@ -523,7 +480,7 @@ export default abstract class BaseAPI {
523
480
  this.apiLog(
524
481
  callbackName,
525
482
  "returned: " + returnValue,
526
- global_constants.LOG_LEVEL_INFO,
483
+ APIConstants.global.LOG_LEVEL_INFO,
527
484
  );
528
485
 
529
486
  return returnValue;
@@ -553,104 +510,6 @@ export default abstract class BaseAPI {
553
510
  return true;
554
511
  }
555
512
 
556
- /**
557
- * Logging for all SCORM actions
558
- *
559
- * @param {string} functionName
560
- * @param {string} logMessage
561
- * @param {number}messageLevel
562
- * @param {string} CMIElement
563
- */
564
- apiLog(
565
- functionName: string,
566
- logMessage: string,
567
- messageLevel: number,
568
- CMIElement?: string,
569
- ) {
570
- logMessage = this.formatMessage(functionName, logMessage, CMIElement);
571
-
572
- if (messageLevel >= this.apiLogLevel) {
573
- this.settings.onLogMessage(messageLevel, logMessage);
574
- }
575
- }
576
-
577
- /**
578
- * Formats the SCORM messages for easy reading
579
- *
580
- * @param {string} functionName
581
- * @param {string} message
582
- * @param {string} CMIElement
583
- * @return {string}
584
- */
585
- formatMessage(
586
- functionName: string,
587
- message: string,
588
- CMIElement?: string,
589
- ): string {
590
- const baseLength = 20;
591
- let messageString = "";
592
-
593
- messageString += functionName;
594
-
595
- let fillChars = baseLength - messageString.length;
596
-
597
- for (let i = 0; i < fillChars; i++) {
598
- messageString += " ";
599
- }
600
-
601
- messageString += ": ";
602
-
603
- if (CMIElement) {
604
- const CMIElementBaseLength = 70;
605
-
606
- messageString += CMIElement;
607
-
608
- fillChars = CMIElementBaseLength - messageString.length;
609
-
610
- for (let j = 0; j < fillChars; j++) {
611
- messageString += " ";
612
- }
613
- }
614
-
615
- if (message) {
616
- messageString += message;
617
- }
618
-
619
- return messageString;
620
- }
621
-
622
- /**
623
- * Checks to see if {str} contains {tester}
624
- *
625
- * @param {string} str String to check against
626
- * @param {string} tester String to check for
627
- * @return {boolean}
628
- */
629
- stringMatches(str: string, tester: string): boolean {
630
- return str?.match(tester) !== null;
631
- }
632
-
633
- /**
634
- * Check to see if the specific object has the given property
635
- * @param {RefObject} refObject
636
- * @param {string} attribute
637
- * @return {boolean}
638
- * @private
639
- */
640
- private _checkObjectHasProperty(
641
- refObject: RefObject,
642
- attribute: string,
643
- ): boolean {
644
- return (
645
- Object.hasOwnProperty.call(refObject, attribute) ||
646
- Object.getOwnPropertyDescriptor(
647
- Object.getPrototypeOf(refObject),
648
- attribute,
649
- ) != null ||
650
- attribute in refObject
651
- );
652
- }
653
-
654
513
  /**
655
514
  * Returns the message that corresponds to errorNumber
656
515
  * APIs that inherit BaseAPI should override this function
@@ -710,12 +569,12 @@ export default abstract class BaseAPI {
710
569
  value: any,
711
570
  ): string {
712
571
  if (!CMIElement || CMIElement === "") {
713
- return global_constants.SCORM_FALSE;
572
+ return APIConstants.global.SCORM_FALSE;
714
573
  }
715
574
 
716
575
  const structure = CMIElement.split(".");
717
576
  let refObject: RefObject = this;
718
- let returnValue = global_constants.SCORM_FALSE;
577
+ let returnValue = APIConstants.global.SCORM_FALSE;
719
578
  let foundFirstIndex = false;
720
579
 
721
580
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
@@ -727,25 +586,28 @@ export default abstract class BaseAPI {
727
586
  const attribute = structure[idx];
728
587
 
729
588
  if (idx === structure.length - 1) {
730
- if (
731
- scorm2004 &&
732
- attribute.substring(0, 8) === "{target=" &&
733
- typeof refObject._isTargetValid == "function"
734
- ) {
735
- this.throwSCORMError(this._error_codes.READ_ONLY_ELEMENT);
589
+ if (scorm2004 && attribute.substring(0, 8) === "{target=") {
590
+ if (this.isInitialized()) {
591
+ this.throwSCORMError(this._error_codes.READ_ONLY_ELEMENT);
592
+ } else {
593
+ refObject = {
594
+ ...refObject,
595
+ attribute: value,
596
+ };
597
+ }
736
598
  } else if (!this._checkObjectHasProperty(refObject, attribute)) {
737
599
  this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
738
600
  } else {
739
601
  if (
740
- this.isInitialized() &&
741
- this.stringMatches(CMIElement, "\\.correct_responses\\.\\d+")
602
+ stringMatches(CMIElement, "\\.correct_responses\\.\\d+") &&
603
+ this.isInitialized()
742
604
  ) {
743
605
  this.validateCorrectResponse(CMIElement, value);
744
606
  }
745
607
 
746
608
  if (!scorm2004 || this.lastErrorCode === "0") {
747
609
  refObject[attribute] = value;
748
- returnValue = global_constants.SCORM_TRUE;
610
+ returnValue = APIConstants.global.SCORM_TRUE;
749
611
  }
750
612
  }
751
613
  } else {
@@ -790,41 +652,17 @@ export default abstract class BaseAPI {
790
652
  }
791
653
  }
792
654
 
793
- if (returnValue === global_constants.SCORM_FALSE) {
655
+ if (returnValue === APIConstants.global.SCORM_FALSE) {
794
656
  this.apiLog(
795
657
  methodName,
796
658
  `There was an error setting the value for: ${CMIElement}, value of: ${value}`,
797
- global_constants.LOG_LEVEL_WARNING,
659
+ APIConstants.global.LOG_LEVEL_WARNING,
798
660
  );
799
661
  }
800
662
 
801
663
  return returnValue;
802
664
  }
803
665
 
804
- /**
805
- * Abstract method for validating that a response is correct.
806
- *
807
- * @param {string} _CMIElement
808
- * @param {any} _value
809
- */
810
- abstract validateCorrectResponse(_CMIElement: string, _value: any): void;
811
-
812
- /**
813
- * Gets or builds a new child element to add to the array.
814
- * APIs that inherit BaseAPI should override this method.
815
- *
816
- * @param {string} _CMIElement - unused
817
- * @param {*} _value - unused
818
- * @param {boolean} _foundFirstIndex - unused
819
- * @return {BaseCMI|null}
820
- * @abstract
821
- */
822
- abstract getChildElement(
823
- _CMIElement: string,
824
- _value: any,
825
- _foundFirstIndex: boolean,
826
- ): BaseCMI | null;
827
-
828
666
  /**
829
667
  * Gets a value from the CMI Object
830
668
  *
@@ -910,9 +748,9 @@ export default abstract class BaseAPI {
910
748
  if (refObject === null || refObject === undefined) {
911
749
  if (!scorm2004) {
912
750
  if (attribute === "_children") {
913
- this.throwSCORMError(scorm12_error_codes.CHILDREN_ERROR);
751
+ this.throwSCORMError(ErrorCodes.scorm12.CHILDREN_ERROR);
914
752
  } else if (attribute === "_count") {
915
- this.throwSCORMError(scorm12_error_codes.COUNT_ERROR);
753
+ this.throwSCORMError(ErrorCodes.scorm12.COUNT_ERROR);
916
754
  }
917
755
  }
918
756
  } else {
@@ -926,7 +764,7 @@ export default abstract class BaseAPI {
926
764
  * @return {boolean}
927
765
  */
928
766
  isInitialized(): boolean {
929
- return this.currentState === global_constants.STATE_INITIALIZED;
767
+ return this.currentState === APIConstants.global.STATE_INITIALIZED;
930
768
  }
931
769
 
932
770
  /**
@@ -935,7 +773,7 @@ export default abstract class BaseAPI {
935
773
  * @return {boolean}
936
774
  */
937
775
  isNotInitialized(): boolean {
938
- return this.currentState === global_constants.STATE_NOT_INITIALIZED;
776
+ return this.currentState === APIConstants.global.STATE_NOT_INITIALIZED;
939
777
  }
940
778
 
941
779
  /**
@@ -944,7 +782,7 @@ export default abstract class BaseAPI {
944
782
  * @return {boolean}
945
783
  */
946
784
  isTerminated(): boolean {
947
- return this.currentState === global_constants.STATE_TERMINATED;
785
+ return this.currentState === APIConstants.global.STATE_TERMINATED;
948
786
  }
949
787
 
950
788
  /**
@@ -977,7 +815,7 @@ export default abstract class BaseAPI {
977
815
  this.apiLog(
978
816
  "on",
979
817
  `Added event listener: ${this.listenerArray.length}`,
980
- global_constants.LOG_LEVEL_INFO,
818
+ APIConstants.global.LOG_LEVEL_INFO,
981
819
  functionName,
982
820
  );
983
821
  }
@@ -1015,7 +853,7 @@ export default abstract class BaseAPI {
1015
853
  this.apiLog(
1016
854
  "off",
1017
855
  `Removed event listener: ${this.listenerArray.length}`,
1018
- global_constants.LOG_LEVEL_INFO,
856
+ APIConstants.global.LOG_LEVEL_INFO,
1019
857
  functionName,
1020
858
  );
1021
859
  }
@@ -1058,7 +896,7 @@ export default abstract class BaseAPI {
1058
896
  this.apiLog(
1059
897
  functionName,
1060
898
  value,
1061
- global_constants.LOG_LEVEL_INFO,
899
+ APIConstants.global.LOG_LEVEL_INFO,
1062
900
  CMIElement,
1063
901
  );
1064
902
  for (let i = 0; i < this.listenerArray.length; i++) {
@@ -1083,7 +921,7 @@ export default abstract class BaseAPI {
1083
921
  this.apiLog(
1084
922
  "processListeners",
1085
923
  `Processing listener: ${listener.functionName}`,
1086
- global_constants.LOG_LEVEL_INFO,
924
+ APIConstants.global.LOG_LEVEL_INFO,
1087
925
  CMIElement,
1088
926
  );
1089
927
  listener.callback(CMIElement, value);
@@ -1105,7 +943,7 @@ export default abstract class BaseAPI {
1105
943
  this.apiLog(
1106
944
  "throwSCORMError",
1107
945
  errorNumber + ": " + message,
1108
- global_constants.LOG_LEVEL_ERROR,
946
+ APIConstants.global.LOG_LEVEL_ERROR,
1109
947
  );
1110
948
 
1111
949
  this.lastErrorCode = String(errorNumber);
@@ -1117,27 +955,21 @@ export default abstract class BaseAPI {
1117
955
  * @param {string} success
1118
956
  */
1119
957
  clearSCORMError(success: string) {
1120
- if (success !== undefined && success !== global_constants.SCORM_FALSE) {
958
+ if (success !== undefined && success !== APIConstants.global.SCORM_FALSE) {
1121
959
  this.lastErrorCode = "0";
1122
960
  }
1123
961
  }
1124
962
 
1125
- /**
1126
- * Attempts to store the data to the LMS, logs data if no LMS configured
1127
- * APIs that inherit BaseAPI should override this function
1128
- *
1129
- * @param {boolean} _calculateTotalTime
1130
- * @return {ResultObject}
1131
- * @abstract
1132
- */
1133
- abstract storeData(_calculateTotalTime: boolean): ResultObject;
1134
-
1135
963
  /**
1136
964
  * Load the CMI from a flattened JSON object
1137
965
  * @param {RefObject} json
1138
966
  * @param {string} CMIElement
1139
967
  */
1140
- loadFromFlattenedJSON(json: RefObject, CMIElement: string) {
968
+ loadFromFlattenedJSON(json: RefObject, CMIElement?: string) {
969
+ if (!CMIElement) {
970
+ // by default, we start from a blank string because we're expecting each element to start with `cmi`
971
+ CMIElement = "";
972
+ }
1141
973
  if (!this.isNotInitialized()) {
1142
974
  console.error(
1143
975
  "loadFromFlattenedJSON can only be called before the call to lmsInitialize.",
@@ -1146,12 +978,12 @@ export default abstract class BaseAPI {
1146
978
  }
1147
979
 
1148
980
  /**
1149
- * Test match pattern.
981
+ * Tests two strings against a given regular expression pattern and determines a numeric or null result based on the matching criterion.
1150
982
  *
1151
- * @param {string} a
1152
- * @param {string} c
1153
- * @param {RegExp} a_pattern
1154
- * @return {number}
983
+ * @param {string} a - The first string to be tested against the pattern.
984
+ * @param {string} c - The second string to be tested against the pattern.
985
+ * @param {RegExp} a_pattern - The regular expression pattern to test the strings against.
986
+ * @return {number | null} A numeric result based on the matching criterion, or null if the strings do not match the pattern.
1155
987
  */
1156
988
  function testPattern(
1157
989
  a: string,
@@ -1265,8 +1097,10 @@ export default abstract class BaseAPI {
1265
1097
  renderCMIToJSONString(): string {
1266
1098
  const cmi = this.cmi;
1267
1099
  // Do we want/need to return fields that have no set value?
1268
- // return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2);
1269
- return JSON.stringify({ cmi });
1100
+ if (this.settings.sendFullCommit) {
1101
+ return JSON.stringify({ cmi });
1102
+ }
1103
+ return JSON.stringify({ cmi }, (k, v) => (v === undefined ? null : v), 2);
1270
1104
  }
1271
1105
 
1272
1106
  /**
@@ -1274,21 +1108,9 @@ export default abstract class BaseAPI {
1274
1108
  * @return {object}
1275
1109
  */
1276
1110
  renderCMIToJSONObject(): object {
1277
- // Do we want/need to return fields that have no set value?
1278
- // return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2);
1279
1111
  return JSON.parse(this.renderCMIToJSONString());
1280
1112
  }
1281
1113
 
1282
- /**
1283
- * Render the cmi object to the proper format for LMS commit
1284
- * APIs that inherit BaseAPI should override this function
1285
- *
1286
- * @param {boolean} _terminateCommit
1287
- * @return {RefObject|Array}
1288
- * @abstract
1289
- */
1290
- abstract renderCommitCMI(_terminateCommit: boolean): RefObject | Array<any>;
1291
-
1292
1114
  /**
1293
1115
  * Send the request to the LMS
1294
1116
  * @param {string} url
@@ -1296,17 +1118,29 @@ export default abstract class BaseAPI {
1296
1118
  * @param {boolean} immediate
1297
1119
  * @return {ResultObject}
1298
1120
  */
1299
- processHttpRequest(
1121
+ async processHttpRequest(
1300
1122
  url: string,
1301
1123
  params: RefObject | Array<any>,
1302
1124
  immediate: boolean = false,
1303
- ): ResultObject {
1125
+ ): Promise<ResultObject> {
1304
1126
  const api = this;
1305
1127
  const genericError: ResultObject = {
1306
- result: global_constants.SCORM_FALSE,
1128
+ result: APIConstants.global.SCORM_FALSE,
1307
1129
  errorCode: this.error_codes.GENERAL,
1308
1130
  };
1309
1131
 
1132
+ // if we are terminating the module or closing the browser window/tab, we need to make this fetch ASAP.
1133
+ // Some browsers, especially Chrome, do not like synchronous requests to be made when the window is closing.
1134
+ if (immediate) {
1135
+ this.performFetch(url, params).then(async (response) => {
1136
+ await this.transformResponse(response);
1137
+ });
1138
+ return {
1139
+ result: APIConstants.global.SCORM_TRUE,
1140
+ errorCode: 0,
1141
+ };
1142
+ }
1143
+
1310
1144
  const process = async (
1311
1145
  url: string,
1312
1146
  params: RefObject | Array<any>,
@@ -1314,49 +1148,31 @@ export default abstract class BaseAPI {
1314
1148
  ): Promise<ResultObject> => {
1315
1149
  try {
1316
1150
  params = settings.requestHandler(params);
1317
- const response = await fetch(url, {
1318
- method: "POST",
1319
- body:
1320
- params instanceof Array ? params.join("&") : JSON.stringify(params),
1321
- headers: {
1322
- ...settings.xhrHeaders,
1323
- "Content-Type": settings.commitRequestDataType,
1324
- },
1325
- credentials: settings.xhrWithCredentials ? "include" : undefined,
1326
- keepalive: true,
1327
- });
1328
-
1329
- const result =
1330
- typeof settings.responseHandler === "function"
1331
- ? await settings.responseHandler(response)
1332
- : await response.json();
1333
-
1334
- if (
1335
- response.status >= 200 &&
1336
- response.status <= 299 &&
1337
- (result.result === true ||
1338
- result.result === global_constants.SCORM_TRUE)
1339
- ) {
1340
- api.processListeners("CommitSuccess");
1341
- } else {
1342
- api.processListeners("CommitError");
1343
- }
1151
+ const response = await this.performFetch(url, params);
1344
1152
 
1345
- return result;
1153
+ return this.transformResponse(response);
1346
1154
  } catch (e) {
1347
- this.apiLog("processHttpRequest", e, global_constants.LOG_LEVEL_ERROR);
1155
+ this.apiLog(
1156
+ "processHttpRequest",
1157
+ e,
1158
+ APIConstants.global.LOG_LEVEL_ERROR,
1159
+ );
1348
1160
  api.processListeners("CommitError");
1349
1161
  return genericError;
1350
1162
  }
1351
1163
  };
1352
1164
 
1353
- const debouncedProcess = debounce(process, 500, immediate);
1354
- debouncedProcess(url, params, this.settings);
1165
+ if (this.settings.asyncCommit) {
1166
+ const debouncedProcess = debounce(process, 500, immediate);
1167
+ debouncedProcess(url, params, this.settings);
1355
1168
 
1356
- return {
1357
- result: global_constants.SCORM_TRUE,
1358
- errorCode: 0,
1359
- };
1169
+ return {
1170
+ result: APIConstants.global.SCORM_TRUE,
1171
+ errorCode: 0,
1172
+ };
1173
+ } else {
1174
+ return await process(url, params, this.settings);
1175
+ }
1360
1176
  }
1361
1177
 
1362
1178
  /**
@@ -1370,7 +1186,7 @@ export default abstract class BaseAPI {
1370
1186
  this.apiLog(
1371
1187
  "scheduleCommit",
1372
1188
  "scheduled",
1373
- global_constants.LOG_LEVEL_DEBUG,
1189
+ APIConstants.global.LOG_LEVEL_DEBUG,
1374
1190
  "",
1375
1191
  );
1376
1192
  }
@@ -1385,16 +1201,44 @@ export default abstract class BaseAPI {
1385
1201
  this.apiLog(
1386
1202
  "clearScheduledCommit",
1387
1203
  "cleared",
1388
- global_constants.LOG_LEVEL_DEBUG,
1204
+ APIConstants.global.LOG_LEVEL_DEBUG,
1389
1205
  "",
1390
1206
  );
1391
1207
  }
1392
1208
  }
1393
1209
 
1394
- private handleValueAccessException(e: any, returnValue: string) {
1210
+ /**
1211
+ * Check to see if the specific object has the given property
1212
+ * @param {RefObject} refObject
1213
+ * @param {string} attribute
1214
+ * @return {boolean}
1215
+ * @private
1216
+ */
1217
+ private _checkObjectHasProperty(
1218
+ refObject: RefObject,
1219
+ attribute: string,
1220
+ ): boolean {
1221
+ return (
1222
+ Object.hasOwnProperty.call(refObject, attribute) ||
1223
+ Object.getOwnPropertyDescriptor(
1224
+ Object.getPrototypeOf(refObject),
1225
+ attribute,
1226
+ ) != null ||
1227
+ attribute in refObject
1228
+ );
1229
+ }
1230
+
1231
+ /**
1232
+ * Handles the error that occurs when trying to access a value
1233
+ * @param {any} e
1234
+ * @param {string} returnValue
1235
+ * @return {string}
1236
+ * @private
1237
+ */
1238
+ private handleValueAccessException(e: any, returnValue: string): string {
1395
1239
  if (e instanceof ValidationError) {
1396
1240
  this.lastErrorCode = String(e.errorCode);
1397
- returnValue = global_constants.SCORM_FALSE;
1241
+ returnValue = APIConstants.global.SCORM_FALSE;
1398
1242
  } else {
1399
1243
  if (e instanceof Error && e.message) {
1400
1244
  console.error(e.message);
@@ -1405,45 +1249,52 @@ export default abstract class BaseAPI {
1405
1249
  }
1406
1250
  return returnValue;
1407
1251
  }
1408
- }
1409
-
1410
- /**
1411
- * Private class that wraps a timeout call to the commit() function
1412
- */
1413
- class ScheduledCommit {
1414
- private _API;
1415
- private _cancelled = false;
1416
- private readonly _timeout;
1417
- private readonly _callback;
1418
1252
 
1419
1253
  /**
1420
- * Constructor for ScheduledCommit
1421
- * @param {BaseAPI} API
1422
- * @param {number} when
1423
- * @param {string} callback
1254
+ * Perform the fetch request to the LMS
1255
+ * @param {string} url
1256
+ * @param {RefObject|Array} params
1257
+ * @return {Promise<Response>}
1258
+ * @private
1424
1259
  */
1425
- constructor(API: any, when: number, callback: string) {
1426
- this._API = API;
1427
- this._timeout = setTimeout(this.wrapper.bind(this), when);
1428
- this._callback = callback;
1260
+ private async performFetch(
1261
+ url: string,
1262
+ params: RefObject | Array<any>,
1263
+ ): Promise<Response> {
1264
+ return fetch(url, {
1265
+ method: "POST",
1266
+ body: params instanceof Array ? params.join("&") : JSON.stringify(params),
1267
+ headers: {
1268
+ ...this.settings.xhrHeaders,
1269
+ "Content-Type": this.settings.commitRequestDataType,
1270
+ },
1271
+ credentials: this.settings.xhrWithCredentials ? "include" : undefined,
1272
+ keepalive: true,
1273
+ });
1429
1274
  }
1430
1275
 
1431
1276
  /**
1432
- * Cancel any currently scheduled commit
1277
+ * Transforms the response from the LMS to a ResultObject
1278
+ * @param {Response} response
1279
+ * @return {Promise<ResultObject>}
1280
+ * @private
1433
1281
  */
1434
- cancel() {
1435
- this._cancelled = true;
1436
- if (this._timeout) {
1437
- clearTimeout(this._timeout);
1438
- }
1439
- }
1282
+ private async transformResponse(response: Response): Promise<ResultObject> {
1283
+ const result =
1284
+ typeof this.settings.responseHandler === "function"
1285
+ ? await this.settings.responseHandler(response)
1286
+ : await response.json();
1440
1287
 
1441
- /**
1442
- * Wrap the API commit call to check if the call has already been cancelled
1443
- */
1444
- wrapper() {
1445
- if (!this._cancelled) {
1446
- this._API.commit(this._callback);
1288
+ if (
1289
+ response.status >= 200 &&
1290
+ response.status <= 299 &&
1291
+ (result.result === true ||
1292
+ result.result === APIConstants.global.SCORM_TRUE)
1293
+ ) {
1294
+ this.processListeners("CommitSuccess");
1295
+ } else {
1296
+ this.processListeners("CommitError");
1447
1297
  }
1298
+ return result;
1448
1299
  }
1449
1300
  }