scorm-again 2.0.0 → 2.2.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 +180 -72
  5. package/dist/aicc.js +1520 -1149
  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 +2812 -2205
  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 +1129 -842
  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 +1921 -1564
  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 +20 -17
  22. package/src/AICC.ts +15 -17
  23. package/src/BaseAPI.ts +283 -420
  24. package/src/Scorm12API.ts +133 -41
  25. package/src/Scorm2004API.ts +224 -120
  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 +82 -0
  56. package/src/constants/enums.ts +17 -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 +50 -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 +372 -9
  67. package/test/Scorm2004API.spec.ts +558 -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,25 @@
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 {
9
+ RefObject,
10
+ CommitObject,
11
+ ResultObject,
12
+ Settings,
13
+ } from "./types/api_types";
14
+ import { DefaultSettings } from "./constants/default_settings";
15
+ import { IBaseAPI } from "./interfaces/IBaseAPI";
16
+ import { ScheduledCommit } from "./helpers/scheduled_commit";
146
17
 
147
18
  /**
148
19
  * Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
149
20
  * abstract, and never initialized on its own.
150
21
  */
151
- export default abstract class BaseAPI {
22
+ export default abstract class BaseAPI implements IBaseAPI {
152
23
  private _timeout?: ScheduledCommit;
153
24
  private readonly _error_codes: ErrorCode;
154
25
  private _settings: Settings = DefaultSettings;
@@ -163,7 +34,7 @@ export default abstract class BaseAPI {
163
34
  if (new.target === BaseAPI) {
164
35
  throw new TypeError("Cannot construct BaseAPI instances directly");
165
36
  }
166
- this.currentState = global_constants.STATE_NOT_INITIALIZED;
37
+ this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
167
38
  this.lastErrorCode = "0";
168
39
  this.listenerArray = [];
169
40
 
@@ -185,6 +56,21 @@ export default abstract class BaseAPI {
185
56
  public apiLogLevel: number;
186
57
  public selfReportSessionTime: boolean;
187
58
 
59
+ abstract reset(settings?: Settings): void;
60
+
61
+ /**
62
+ * Common reset method for all APIs. New settings are merged with the existing settings.
63
+ * @param {Settings} settings
64
+ * @protected
65
+ */
66
+ commonReset(settings?: Settings): void {
67
+ this.settings = { ...this.settings, ...settings };
68
+
69
+ this.currentState = APIConstants.global.STATE_NOT_INITIALIZED;
70
+ this.lastErrorCode = "0";
71
+ this.listenerArray = [];
72
+ }
73
+
188
74
  /**
189
75
  * Initialize the API
190
76
  * @param {string} callbackName
@@ -197,7 +83,7 @@ export default abstract class BaseAPI {
197
83
  initializeMessage?: string,
198
84
  terminationMessage?: string,
199
85
  ): string {
200
- let returnValue = global_constants.SCORM_FALSE;
86
+ let returnValue = APIConstants.global.SCORM_FALSE;
201
87
 
202
88
  if (this.isInitialized()) {
203
89
  this.throwSCORMError(this._error_codes.INITIALIZED, initializeMessage);
@@ -208,16 +94,16 @@ export default abstract class BaseAPI {
208
94
  this.cmi.setStartTime();
209
95
  }
210
96
 
211
- this.currentState = global_constants.STATE_INITIALIZED;
97
+ this.currentState = APIConstants.global.STATE_INITIALIZED;
212
98
  this.lastErrorCode = "0";
213
- returnValue = global_constants.SCORM_TRUE;
99
+ returnValue = APIConstants.global.SCORM_TRUE;
214
100
  this.processListeners(callbackName);
215
101
  }
216
102
 
217
103
  this.apiLog(
218
104
  callbackName,
219
105
  "returned: " + returnValue,
220
- global_constants.LOG_LEVEL_INFO,
106
+ APIConstants.global.LOG_LEVEL_INFO,
221
107
  );
222
108
  this.clearSCORMError(returnValue);
223
109
 
@@ -240,6 +126,78 @@ export default abstract class BaseAPI {
240
126
 
241
127
  abstract lmsGetDiagnostic(CMIErrorCode: string | number): string;
242
128
 
129
+ /**
130
+ * Abstract method for validating that a response is correct.
131
+ *
132
+ * @param {string} _CMIElement
133
+ * @param {any} _value
134
+ */
135
+ abstract validateCorrectResponse(_CMIElement: string, _value: any): void;
136
+
137
+ /**
138
+ * Gets or builds a new child element to add to the array.
139
+ * APIs that inherit BaseAPI should override this method.
140
+ *
141
+ * @param {string} _CMIElement - unused
142
+ * @param {*} _value - unused
143
+ * @param {boolean} _foundFirstIndex - unused
144
+ * @return {BaseCMI|null}
145
+ * @abstract
146
+ */
147
+ abstract getChildElement(
148
+ _CMIElement: string,
149
+ _value: any,
150
+ _foundFirstIndex: boolean,
151
+ ): BaseCMI | null;
152
+
153
+ /**
154
+ * Attempts to store the data to the LMS, logs data if no LMS configured
155
+ * APIs that inherit BaseAPI should override this function
156
+ *
157
+ * @param {boolean} _calculateTotalTime
158
+ * @return {ResultObject}
159
+ * @abstract
160
+ */
161
+ abstract storeData(_calculateTotalTime: boolean): Promise<ResultObject>;
162
+
163
+ /**
164
+ * Render the cmi object to the proper format for LMS commit
165
+ * APIs that inherit BaseAPI should override this function
166
+ *
167
+ * @param {boolean} _terminateCommit
168
+ * @return {RefObject|Array}
169
+ * @abstract
170
+ */
171
+ abstract renderCommitCMI(_terminateCommit: boolean): RefObject | Array<any>;
172
+
173
+ /**
174
+ * Render the commit object to the shortened format for LMS commit
175
+ * @param {boolean} _terminateCommit
176
+ * @return {CommitObject}
177
+ */
178
+ abstract renderCommitObject(_terminateCommit: boolean): CommitObject;
179
+
180
+ /**
181
+ * Logging for all SCORM actions
182
+ *
183
+ * @param {string} functionName
184
+ * @param {string} logMessage
185
+ * @param {number} messageLevel
186
+ * @param {string} CMIElement
187
+ */
188
+ apiLog(
189
+ functionName: string,
190
+ logMessage: string,
191
+ messageLevel: number,
192
+ CMIElement?: string,
193
+ ) {
194
+ logMessage = formatMessage(functionName, logMessage, CMIElement);
195
+
196
+ if (messageLevel >= this.apiLogLevel) {
197
+ this.settings.onLogMessage(messageLevel, logMessage);
198
+ }
199
+ }
200
+
243
201
  /**
244
202
  * Getter for _error_codes
245
203
  * @return {ErrorCode}
@@ -270,8 +228,11 @@ export default abstract class BaseAPI {
270
228
  * @param {boolean} checkTerminated
271
229
  * @return {string}
272
230
  */
273
- terminate(callbackName: string, checkTerminated: boolean): string {
274
- let returnValue = global_constants.SCORM_FALSE;
231
+ async terminate(
232
+ callbackName: string,
233
+ checkTerminated: boolean,
234
+ ): Promise<string> {
235
+ let returnValue = APIConstants.global.SCORM_FALSE;
275
236
 
276
237
  if (
277
238
  this.checkState(
@@ -280,27 +241,27 @@ export default abstract class BaseAPI {
280
241
  this._error_codes.MULTIPLE_TERMINATION,
281
242
  )
282
243
  ) {
283
- this.currentState = global_constants.STATE_TERMINATED;
244
+ this.currentState = APIConstants.global.STATE_TERMINATED;
284
245
 
285
- const result: ResultObject = this.storeData(true);
246
+ const result: ResultObject = await this.storeData(true);
286
247
  if (typeof result.errorCode !== "undefined" && result.errorCode > 0) {
287
248
  this.throwSCORMError(result.errorCode);
288
249
  }
289
250
  returnValue =
290
251
  typeof result !== "undefined" && result.result
291
252
  ? result.result
292
- : global_constants.SCORM_FALSE;
253
+ : APIConstants.global.SCORM_FALSE;
293
254
 
294
255
  if (checkTerminated) this.lastErrorCode = "0";
295
256
 
296
- returnValue = global_constants.SCORM_TRUE;
257
+ returnValue = APIConstants.global.SCORM_TRUE;
297
258
  this.processListeners(callbackName);
298
259
  }
299
260
 
300
261
  this.apiLog(
301
262
  callbackName,
302
263
  "returned: " + returnValue,
303
- global_constants.LOG_LEVEL_INFO,
264
+ APIConstants.global.LOG_LEVEL_INFO,
304
265
  );
305
266
  this.clearSCORMError(returnValue);
306
267
 
@@ -341,9 +302,14 @@ export default abstract class BaseAPI {
341
302
  this.apiLog(
342
303
  callbackName,
343
304
  ": returned: " + returnValue,
344
- global_constants.LOG_LEVEL_INFO,
305
+ APIConstants.global.LOG_LEVEL_INFO,
345
306
  CMIElement,
346
307
  );
308
+
309
+ if (returnValue === undefined) {
310
+ return "";
311
+ }
312
+
347
313
  this.clearSCORMError(returnValue);
348
314
 
349
315
  return returnValue;
@@ -369,7 +335,7 @@ export default abstract class BaseAPI {
369
335
  if (value !== undefined) {
370
336
  value = String(value);
371
337
  }
372
- let returnValue: string = global_constants.SCORM_FALSE;
338
+ let returnValue: string = APIConstants.global.SCORM_FALSE;
373
339
 
374
340
  if (
375
341
  this.checkState(
@@ -388,7 +354,7 @@ export default abstract class BaseAPI {
388
354
  }
389
355
 
390
356
  if (returnValue === undefined) {
391
- returnValue = global_constants.SCORM_FALSE;
357
+ returnValue = APIConstants.global.SCORM_FALSE;
392
358
  }
393
359
 
394
360
  // If we didn't have any errors while setting the data, go ahead and
@@ -405,7 +371,7 @@ export default abstract class BaseAPI {
405
371
  this.apiLog(
406
372
  callbackName,
407
373
  ": " + value + ": result: " + returnValue,
408
- global_constants.LOG_LEVEL_INFO,
374
+ APIConstants.global.LOG_LEVEL_INFO,
409
375
  CMIElement,
410
376
  );
411
377
  this.clearSCORMError(returnValue);
@@ -419,10 +385,13 @@ export default abstract class BaseAPI {
419
385
  * @param {boolean} checkTerminated
420
386
  * @return {string}
421
387
  */
422
- commit(callbackName: string, checkTerminated: boolean = false): string {
388
+ async commit(
389
+ callbackName: string,
390
+ checkTerminated: boolean = false,
391
+ ): Promise<string> {
423
392
  this.clearScheduledCommit();
424
393
 
425
- let returnValue = global_constants.SCORM_FALSE;
394
+ let returnValue = APIConstants.global.SCORM_FALSE;
426
395
 
427
396
  if (
428
397
  this.checkState(
@@ -431,19 +400,19 @@ export default abstract class BaseAPI {
431
400
  this._error_codes.COMMIT_AFTER_TERM,
432
401
  )
433
402
  ) {
434
- const result = this.storeData(false);
403
+ const result = await this.storeData(false);
435
404
  if (result.errorCode && result.errorCode > 0) {
436
405
  this.throwSCORMError(result.errorCode);
437
406
  }
438
407
  returnValue =
439
408
  typeof result !== "undefined" && result.result
440
409
  ? result.result
441
- : global_constants.SCORM_FALSE;
410
+ : APIConstants.global.SCORM_FALSE;
442
411
 
443
412
  this.apiLog(
444
413
  callbackName,
445
414
  " Result: " + returnValue,
446
- global_constants.LOG_LEVEL_DEBUG,
415
+ APIConstants.global.LOG_LEVEL_DEBUG,
447
416
  "HttpRequest",
448
417
  );
449
418
 
@@ -455,7 +424,7 @@ export default abstract class BaseAPI {
455
424
  this.apiLog(
456
425
  callbackName,
457
426
  "returned: " + returnValue,
458
- global_constants.LOG_LEVEL_INFO,
427
+ APIConstants.global.LOG_LEVEL_INFO,
459
428
  );
460
429
  this.clearSCORMError(returnValue);
461
430
 
@@ -475,7 +444,7 @@ export default abstract class BaseAPI {
475
444
  this.apiLog(
476
445
  callbackName,
477
446
  "returned: " + returnValue,
478
- global_constants.LOG_LEVEL_INFO,
447
+ APIConstants.global.LOG_LEVEL_INFO,
479
448
  );
480
449
 
481
450
  return returnValue;
@@ -499,7 +468,7 @@ export default abstract class BaseAPI {
499
468
  this.apiLog(
500
469
  callbackName,
501
470
  "returned: " + returnValue,
502
- global_constants.LOG_LEVEL_INFO,
471
+ APIConstants.global.LOG_LEVEL_INFO,
503
472
  );
504
473
 
505
474
  return returnValue;
@@ -523,7 +492,7 @@ export default abstract class BaseAPI {
523
492
  this.apiLog(
524
493
  callbackName,
525
494
  "returned: " + returnValue,
526
- global_constants.LOG_LEVEL_INFO,
495
+ APIConstants.global.LOG_LEVEL_INFO,
527
496
  );
528
497
 
529
498
  return returnValue;
@@ -553,104 +522,6 @@ export default abstract class BaseAPI {
553
522
  return true;
554
523
  }
555
524
 
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
525
  /**
655
526
  * Returns the message that corresponds to errorNumber
656
527
  * APIs that inherit BaseAPI should override this function
@@ -710,12 +581,12 @@ export default abstract class BaseAPI {
710
581
  value: any,
711
582
  ): string {
712
583
  if (!CMIElement || CMIElement === "") {
713
- return global_constants.SCORM_FALSE;
584
+ return APIConstants.global.SCORM_FALSE;
714
585
  }
715
586
 
716
587
  const structure = CMIElement.split(".");
717
588
  let refObject: RefObject = this;
718
- let returnValue = global_constants.SCORM_FALSE;
589
+ let returnValue = APIConstants.global.SCORM_FALSE;
719
590
  let foundFirstIndex = false;
720
591
 
721
592
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
@@ -727,25 +598,28 @@ export default abstract class BaseAPI {
727
598
  const attribute = structure[idx];
728
599
 
729
600
  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);
601
+ if (scorm2004 && attribute.substring(0, 8) === "{target=") {
602
+ if (this.isInitialized()) {
603
+ this.throwSCORMError(this._error_codes.READ_ONLY_ELEMENT);
604
+ } else {
605
+ refObject = {
606
+ ...refObject,
607
+ attribute: value,
608
+ };
609
+ }
736
610
  } else if (!this._checkObjectHasProperty(refObject, attribute)) {
737
611
  this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
738
612
  } else {
739
613
  if (
740
- this.isInitialized() &&
741
- this.stringMatches(CMIElement, "\\.correct_responses\\.\\d+")
614
+ stringMatches(CMIElement, "\\.correct_responses\\.\\d+") &&
615
+ this.isInitialized()
742
616
  ) {
743
617
  this.validateCorrectResponse(CMIElement, value);
744
618
  }
745
619
 
746
620
  if (!scorm2004 || this.lastErrorCode === "0") {
747
621
  refObject[attribute] = value;
748
- returnValue = global_constants.SCORM_TRUE;
622
+ returnValue = APIConstants.global.SCORM_TRUE;
749
623
  }
750
624
  }
751
625
  } else {
@@ -790,41 +664,17 @@ export default abstract class BaseAPI {
790
664
  }
791
665
  }
792
666
 
793
- if (returnValue === global_constants.SCORM_FALSE) {
667
+ if (returnValue === APIConstants.global.SCORM_FALSE) {
794
668
  this.apiLog(
795
669
  methodName,
796
670
  `There was an error setting the value for: ${CMIElement}, value of: ${value}`,
797
- global_constants.LOG_LEVEL_WARNING,
671
+ APIConstants.global.LOG_LEVEL_WARNING,
798
672
  );
799
673
  }
800
674
 
801
675
  return returnValue;
802
676
  }
803
677
 
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
678
  /**
829
679
  * Gets a value from the CMI Object
830
680
  *
@@ -910,9 +760,9 @@ export default abstract class BaseAPI {
910
760
  if (refObject === null || refObject === undefined) {
911
761
  if (!scorm2004) {
912
762
  if (attribute === "_children") {
913
- this.throwSCORMError(scorm12_error_codes.CHILDREN_ERROR);
763
+ this.throwSCORMError(ErrorCodes.scorm12.CHILDREN_ERROR);
914
764
  } else if (attribute === "_count") {
915
- this.throwSCORMError(scorm12_error_codes.COUNT_ERROR);
765
+ this.throwSCORMError(ErrorCodes.scorm12.COUNT_ERROR);
916
766
  }
917
767
  }
918
768
  } else {
@@ -926,7 +776,7 @@ export default abstract class BaseAPI {
926
776
  * @return {boolean}
927
777
  */
928
778
  isInitialized(): boolean {
929
- return this.currentState === global_constants.STATE_INITIALIZED;
779
+ return this.currentState === APIConstants.global.STATE_INITIALIZED;
930
780
  }
931
781
 
932
782
  /**
@@ -935,7 +785,7 @@ export default abstract class BaseAPI {
935
785
  * @return {boolean}
936
786
  */
937
787
  isNotInitialized(): boolean {
938
- return this.currentState === global_constants.STATE_NOT_INITIALIZED;
788
+ return this.currentState === APIConstants.global.STATE_NOT_INITIALIZED;
939
789
  }
940
790
 
941
791
  /**
@@ -944,7 +794,7 @@ export default abstract class BaseAPI {
944
794
  * @return {boolean}
945
795
  */
946
796
  isTerminated(): boolean {
947
- return this.currentState === global_constants.STATE_TERMINATED;
797
+ return this.currentState === APIConstants.global.STATE_TERMINATED;
948
798
  }
949
799
 
950
800
  /**
@@ -977,7 +827,7 @@ export default abstract class BaseAPI {
977
827
  this.apiLog(
978
828
  "on",
979
829
  `Added event listener: ${this.listenerArray.length}`,
980
- global_constants.LOG_LEVEL_INFO,
830
+ APIConstants.global.LOG_LEVEL_INFO,
981
831
  functionName,
982
832
  );
983
833
  }
@@ -1015,7 +865,7 @@ export default abstract class BaseAPI {
1015
865
  this.apiLog(
1016
866
  "off",
1017
867
  `Removed event listener: ${this.listenerArray.length}`,
1018
- global_constants.LOG_LEVEL_INFO,
868
+ APIConstants.global.LOG_LEVEL_INFO,
1019
869
  functionName,
1020
870
  );
1021
871
  }
@@ -1058,7 +908,7 @@ export default abstract class BaseAPI {
1058
908
  this.apiLog(
1059
909
  functionName,
1060
910
  value,
1061
- global_constants.LOG_LEVEL_INFO,
911
+ APIConstants.global.LOG_LEVEL_INFO,
1062
912
  CMIElement,
1063
913
  );
1064
914
  for (let i = 0; i < this.listenerArray.length; i++) {
@@ -1083,7 +933,7 @@ export default abstract class BaseAPI {
1083
933
  this.apiLog(
1084
934
  "processListeners",
1085
935
  `Processing listener: ${listener.functionName}`,
1086
- global_constants.LOG_LEVEL_INFO,
936
+ APIConstants.global.LOG_LEVEL_INFO,
1087
937
  CMIElement,
1088
938
  );
1089
939
  listener.callback(CMIElement, value);
@@ -1105,7 +955,7 @@ export default abstract class BaseAPI {
1105
955
  this.apiLog(
1106
956
  "throwSCORMError",
1107
957
  errorNumber + ": " + message,
1108
- global_constants.LOG_LEVEL_ERROR,
958
+ APIConstants.global.LOG_LEVEL_ERROR,
1109
959
  );
1110
960
 
1111
961
  this.lastErrorCode = String(errorNumber);
@@ -1117,27 +967,21 @@ export default abstract class BaseAPI {
1117
967
  * @param {string} success
1118
968
  */
1119
969
  clearSCORMError(success: string) {
1120
- if (success !== undefined && success !== global_constants.SCORM_FALSE) {
970
+ if (success !== undefined && success !== APIConstants.global.SCORM_FALSE) {
1121
971
  this.lastErrorCode = "0";
1122
972
  }
1123
973
  }
1124
974
 
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
975
  /**
1136
976
  * Load the CMI from a flattened JSON object
1137
977
  * @param {RefObject} json
1138
978
  * @param {string} CMIElement
1139
979
  */
1140
- loadFromFlattenedJSON(json: RefObject, CMIElement: string) {
980
+ loadFromFlattenedJSON(json: RefObject, CMIElement?: string) {
981
+ if (!CMIElement) {
982
+ // by default, we start from a blank string because we're expecting each element to start with `cmi`
983
+ CMIElement = "";
984
+ }
1141
985
  if (!this.isNotInitialized()) {
1142
986
  console.error(
1143
987
  "loadFromFlattenedJSON can only be called before the call to lmsInitialize.",
@@ -1146,12 +990,12 @@ export default abstract class BaseAPI {
1146
990
  }
1147
991
 
1148
992
  /**
1149
- * Test match pattern.
993
+ * Tests two strings against a given regular expression pattern and determines a numeric or null result based on the matching criterion.
1150
994
  *
1151
- * @param {string} a
1152
- * @param {string} c
1153
- * @param {RegExp} a_pattern
1154
- * @return {number}
995
+ * @param {string} a - The first string to be tested against the pattern.
996
+ * @param {string} c - The second string to be tested against the pattern.
997
+ * @param {RegExp} a_pattern - The regular expression pattern to test the strings against.
998
+ * @return {number | null} A numeric result based on the matching criterion, or null if the strings do not match the pattern.
1155
999
  */
1156
1000
  function testPattern(
1157
1001
  a: string,
@@ -1265,8 +1109,10 @@ export default abstract class BaseAPI {
1265
1109
  renderCMIToJSONString(): string {
1266
1110
  const cmi = this.cmi;
1267
1111
  // 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 });
1112
+ if (this.settings.sendFullCommit) {
1113
+ return JSON.stringify({ cmi });
1114
+ }
1115
+ return JSON.stringify({ cmi }, (k, v) => (v === undefined ? null : v), 2);
1270
1116
  }
1271
1117
 
1272
1118
  /**
@@ -1274,89 +1120,71 @@ export default abstract class BaseAPI {
1274
1120
  * @return {object}
1275
1121
  */
1276
1122
  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
1123
  return JSON.parse(this.renderCMIToJSONString());
1280
1124
  }
1281
1125
 
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
1126
  /**
1293
1127
  * Send the request to the LMS
1294
1128
  * @param {string} url
1295
- * @param {RefObject|Array} params
1129
+ * @param {CommitObject|RefObject|Array} params
1296
1130
  * @param {boolean} immediate
1297
1131
  * @return {ResultObject}
1298
1132
  */
1299
- processHttpRequest(
1133
+ async processHttpRequest(
1300
1134
  url: string,
1301
- params: RefObject | Array<any>,
1135
+ params: CommitObject | RefObject | Array<any>,
1302
1136
  immediate: boolean = false,
1303
- ): ResultObject {
1137
+ ): Promise<ResultObject> {
1304
1138
  const api = this;
1305
1139
  const genericError: ResultObject = {
1306
- result: global_constants.SCORM_FALSE,
1140
+ result: APIConstants.global.SCORM_FALSE,
1307
1141
  errorCode: this.error_codes.GENERAL,
1308
1142
  };
1309
1143
 
1144
+ // if we are terminating the module or closing the browser window/tab, we need to make this fetch ASAP.
1145
+ // Some browsers, especially Chrome, do not like synchronous requests to be made when the window is closing.
1146
+ if (immediate) {
1147
+ this.performFetch(url, params).then(async (response) => {
1148
+ await this.transformResponse(response);
1149
+ });
1150
+ return {
1151
+ result: APIConstants.global.SCORM_TRUE,
1152
+ errorCode: 0,
1153
+ };
1154
+ }
1155
+
1310
1156
  const process = async (
1311
1157
  url: string,
1312
- params: RefObject | Array<any>,
1158
+ params: CommitObject | RefObject | Array<any>,
1313
1159
  settings: Settings,
1314
1160
  ): Promise<ResultObject> => {
1315
1161
  try {
1316
1162
  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();
1163
+ const response = await this.performFetch(url, params);
1333
1164
 
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
- }
1344
-
1345
- return result;
1165
+ return this.transformResponse(response);
1346
1166
  } catch (e) {
1347
- this.apiLog("processHttpRequest", e, global_constants.LOG_LEVEL_ERROR);
1167
+ this.apiLog(
1168
+ "processHttpRequest",
1169
+ e,
1170
+ APIConstants.global.LOG_LEVEL_ERROR,
1171
+ );
1348
1172
  api.processListeners("CommitError");
1349
1173
  return genericError;
1350
1174
  }
1351
1175
  };
1352
1176
 
1353
- const debouncedProcess = debounce(process, 500, immediate);
1354
- debouncedProcess(url, params, this.settings);
1177
+ if (this.settings.asyncCommit) {
1178
+ const debouncedProcess = debounce(process, 500, immediate);
1179
+ debouncedProcess(url, params, this.settings);
1355
1180
 
1356
- return {
1357
- result: global_constants.SCORM_TRUE,
1358
- errorCode: 0,
1359
- };
1181
+ return {
1182
+ result: APIConstants.global.SCORM_TRUE,
1183
+ errorCode: 0,
1184
+ };
1185
+ } else {
1186
+ return await process(url, params, this.settings);
1187
+ }
1360
1188
  }
1361
1189
 
1362
1190
  /**
@@ -1370,7 +1198,7 @@ export default abstract class BaseAPI {
1370
1198
  this.apiLog(
1371
1199
  "scheduleCommit",
1372
1200
  "scheduled",
1373
- global_constants.LOG_LEVEL_DEBUG,
1201
+ APIConstants.global.LOG_LEVEL_DEBUG,
1374
1202
  "",
1375
1203
  );
1376
1204
  }
@@ -1385,16 +1213,44 @@ export default abstract class BaseAPI {
1385
1213
  this.apiLog(
1386
1214
  "clearScheduledCommit",
1387
1215
  "cleared",
1388
- global_constants.LOG_LEVEL_DEBUG,
1216
+ APIConstants.global.LOG_LEVEL_DEBUG,
1389
1217
  "",
1390
1218
  );
1391
1219
  }
1392
1220
  }
1393
1221
 
1394
- private handleValueAccessException(e: any, returnValue: string) {
1222
+ /**
1223
+ * Check to see if the specific object has the given property
1224
+ * @param {RefObject} refObject
1225
+ * @param {string} attribute
1226
+ * @return {boolean}
1227
+ * @private
1228
+ */
1229
+ private _checkObjectHasProperty(
1230
+ refObject: RefObject,
1231
+ attribute: string,
1232
+ ): boolean {
1233
+ return (
1234
+ Object.hasOwnProperty.call(refObject, attribute) ||
1235
+ Object.getOwnPropertyDescriptor(
1236
+ Object.getPrototypeOf(refObject),
1237
+ attribute,
1238
+ ) != null ||
1239
+ attribute in refObject
1240
+ );
1241
+ }
1242
+
1243
+ /**
1244
+ * Handles the error that occurs when trying to access a value
1245
+ * @param {any} e
1246
+ * @param {string} returnValue
1247
+ * @return {string}
1248
+ * @private
1249
+ */
1250
+ private handleValueAccessException(e: any, returnValue: string): string {
1395
1251
  if (e instanceof ValidationError) {
1396
1252
  this.lastErrorCode = String(e.errorCode);
1397
- returnValue = global_constants.SCORM_FALSE;
1253
+ returnValue = APIConstants.global.SCORM_FALSE;
1398
1254
  } else {
1399
1255
  if (e instanceof Error && e.message) {
1400
1256
  console.error(e.message);
@@ -1405,45 +1261,52 @@ export default abstract class BaseAPI {
1405
1261
  }
1406
1262
  return returnValue;
1407
1263
  }
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
1264
 
1419
1265
  /**
1420
- * Constructor for ScheduledCommit
1421
- * @param {BaseAPI} API
1422
- * @param {number} when
1423
- * @param {string} callback
1266
+ * Perform the fetch request to the LMS
1267
+ * @param {string} url
1268
+ * @param {RefObject|Array} params
1269
+ * @return {Promise<Response>}
1270
+ * @private
1424
1271
  */
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;
1272
+ private async performFetch(
1273
+ url: string,
1274
+ params: RefObject | Array<any>,
1275
+ ): Promise<Response> {
1276
+ return fetch(url, {
1277
+ method: "POST",
1278
+ body: params instanceof Array ? params.join("&") : JSON.stringify(params),
1279
+ headers: {
1280
+ ...this.settings.xhrHeaders,
1281
+ "Content-Type": this.settings.commitRequestDataType,
1282
+ },
1283
+ credentials: this.settings.xhrWithCredentials ? "include" : undefined,
1284
+ keepalive: true,
1285
+ });
1429
1286
  }
1430
1287
 
1431
1288
  /**
1432
- * Cancel any currently scheduled commit
1289
+ * Transforms the response from the LMS to a ResultObject
1290
+ * @param {Response} response
1291
+ * @return {Promise<ResultObject>}
1292
+ * @private
1433
1293
  */
1434
- cancel() {
1435
- this._cancelled = true;
1436
- if (this._timeout) {
1437
- clearTimeout(this._timeout);
1438
- }
1439
- }
1294
+ private async transformResponse(response: Response): Promise<ResultObject> {
1295
+ const result =
1296
+ typeof this.settings.responseHandler === "function"
1297
+ ? await this.settings.responseHandler(response)
1298
+ : await response.json();
1440
1299
 
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);
1300
+ if (
1301
+ response.status >= 200 &&
1302
+ response.status <= 299 &&
1303
+ (result.result === true ||
1304
+ result.result === APIConstants.global.SCORM_TRUE)
1305
+ ) {
1306
+ this.processListeners("CommitSuccess");
1307
+ } else {
1308
+ this.processListeners("CommitError");
1447
1309
  }
1310
+ return result;
1448
1311
  }
1449
1312
  }