scorm-again 1.7.1 → 2.0.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 (85) hide show
  1. package/.babelrc +18 -7
  2. package/.github/dependabot.yml +5 -0
  3. package/.github/workflows/main.yml +79 -0
  4. package/.jsdoc.json +4 -5
  5. package/.mocharc.json +8 -0
  6. package/.run/Mocha Unit Tests.run.xml +5 -2
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.md +14 -1
  9. package/dist/aicc.js +3661 -7170
  10. package/dist/aicc.js.map +1 -1
  11. package/dist/aicc.min.js +2 -40
  12. package/dist/aicc.min.js.map +1 -0
  13. package/dist/scorm-again.js +5671 -10695
  14. package/dist/scorm-again.js.map +1 -1
  15. package/dist/scorm-again.min.js +2 -52
  16. package/dist/scorm-again.min.js.map +1 -0
  17. package/dist/scorm12.js +2871 -5433
  18. package/dist/scorm12.js.map +1 -1
  19. package/dist/scorm12.min.js +2 -34
  20. package/dist/scorm12.min.js.map +1 -0
  21. package/dist/scorm2004.js +3868 -6797
  22. package/dist/scorm2004.js.map +1 -1
  23. package/dist/scorm2004.min.js +2 -40
  24. package/dist/scorm2004.min.js.map +1 -0
  25. package/eslint.config.js +21 -0
  26. package/package.json +72 -34
  27. package/results.json +34254 -0
  28. package/src/{AICC.js → AICC.ts} +27 -21
  29. package/src/BaseAPI.ts +1449 -0
  30. package/src/Scorm12API.ts +360 -0
  31. package/src/{Scorm2004API.js → Scorm2004API.ts} +245 -163
  32. package/src/cmi/aicc_cmi.ts +1248 -0
  33. package/src/cmi/common.ts +411 -0
  34. package/src/cmi/scorm12_cmi.ts +1426 -0
  35. package/src/cmi/scorm2004_cmi.ts +1874 -0
  36. package/src/constants/api_constants.ts +318 -0
  37. package/src/constants/error_codes.ts +88 -0
  38. package/src/constants/language_constants.ts +394 -0
  39. package/src/constants/regex.ts +97 -0
  40. package/src/constants/{response_constants.js → response_constants.ts} +67 -62
  41. package/src/exceptions.ts +133 -0
  42. package/src/exports/aicc.js +1 -1
  43. package/src/exports/scorm-again.js +3 -3
  44. package/src/exports/scorm12.js +1 -1
  45. package/src/exports/scorm2004.js +1 -1
  46. package/src/{utilities.js → utilities.ts} +114 -74
  47. package/tea.yaml +6 -0
  48. package/test/{AICC.spec.js → AICC.spec.ts} +70 -72
  49. package/test/Scorm12API.spec.ts +580 -0
  50. package/test/Scorm2004API.spec.ts +812 -0
  51. package/test/api_helpers.ts +176 -0
  52. package/test/cmi/{aicc_cmi.spec.js → aicc_cmi.spec.ts} +193 -209
  53. package/test/cmi/{scorm12_cmi.spec.js → scorm12_cmi.spec.ts} +251 -269
  54. package/test/cmi/scorm2004_cmi.spec.ts +1031 -0
  55. package/test/cmi_helpers.ts +207 -0
  56. package/test/exceptions.spec.ts +79 -0
  57. package/test/field_values.ts +202 -0
  58. package/test/utilities.spec.ts +322 -0
  59. package/tsconfig.json +18 -0
  60. package/webpack.config.js +65 -0
  61. package/.circleci/config.yml +0 -99
  62. package/.codeclimate.yml +0 -7
  63. package/.eslintrc.js +0 -36
  64. package/src/.flowconfig +0 -11
  65. package/src/BaseAPI.js +0 -1275
  66. package/src/Scorm12API.js +0 -308
  67. package/src/cmi/aicc_cmi.js +0 -1141
  68. package/src/cmi/common.js +0 -328
  69. package/src/cmi/scorm12_cmi.js +0 -1312
  70. package/src/cmi/scorm2004_cmi.js +0 -1692
  71. package/src/constants/api_constants.js +0 -218
  72. package/src/constants/error_codes.js +0 -87
  73. package/src/constants/language_constants.js +0 -76
  74. package/src/constants/regex.js +0 -84
  75. package/src/exceptions.js +0 -104
  76. package/test/Scorm12API.spec.js +0 -528
  77. package/test/Scorm2004API.spec.js +0 -775
  78. package/test/abstract_classes.spec.js +0 -17
  79. package/test/api_helpers.js +0 -128
  80. package/test/cmi/scorm2004_cmi.spec.js +0 -1066
  81. package/test/cmi_helpers.js +0 -161
  82. package/test/exceptions.spec.js +0 -71
  83. package/test/field_values.js +0 -353
  84. package/test/utilities.spec.js +0 -339
  85. package/webpack.js +0 -78
package/src/BaseAPI.ts ADDED
@@ -0,0 +1,1449 @@
1
+ import { BaseCMI, CMIArray } from "./cmi/common";
2
+ import { ValidationError } from "./exceptions";
3
+ import ErrorCodes, { ErrorCode } from "./constants/error_codes";
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
+ };
146
+
147
+ /**
148
+ * Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
149
+ * abstract, and never initialized on its own.
150
+ */
151
+ export default abstract class BaseAPI {
152
+ private _timeout?: ScheduledCommit;
153
+ private readonly _error_codes: ErrorCode;
154
+ private _settings: Settings = DefaultSettings;
155
+
156
+ /**
157
+ * Constructor for Base API class. Sets some shared API fields, as well as
158
+ * sets up options for the API.
159
+ * @param {ErrorCode} error_codes
160
+ * @param {Settings} settings
161
+ */
162
+ protected constructor(error_codes: ErrorCode, settings?: Settings) {
163
+ if (new.target === BaseAPI) {
164
+ throw new TypeError("Cannot construct BaseAPI instances directly");
165
+ }
166
+ this.currentState = global_constants.STATE_NOT_INITIALIZED;
167
+ this.lastErrorCode = "0";
168
+ this.listenerArray = [];
169
+
170
+ this._error_codes = error_codes;
171
+
172
+ if (settings) {
173
+ this.settings = settings;
174
+ }
175
+ this.apiLogLevel = this.settings.logLevel;
176
+ this.selfReportSessionTime = this.settings.selfReportSessionTime;
177
+ }
178
+
179
+ public abstract cmi: BaseCMI;
180
+ public startingData?: RefObject;
181
+
182
+ public currentState: number;
183
+ public lastErrorCode: string;
184
+ public listenerArray: any[];
185
+ public apiLogLevel: number;
186
+ public selfReportSessionTime: boolean;
187
+
188
+ /**
189
+ * Initialize the API
190
+ * @param {string} callbackName
191
+ * @param {string} initializeMessage
192
+ * @param {string} terminationMessage
193
+ * @return {string}
194
+ */
195
+ initialize(
196
+ callbackName: string,
197
+ initializeMessage?: string,
198
+ terminationMessage?: string,
199
+ ): string {
200
+ let returnValue = global_constants.SCORM_FALSE;
201
+
202
+ if (this.isInitialized()) {
203
+ this.throwSCORMError(this._error_codes.INITIALIZED, initializeMessage);
204
+ } else if (this.isTerminated()) {
205
+ this.throwSCORMError(this._error_codes.TERMINATED, terminationMessage);
206
+ } else {
207
+ if (this.selfReportSessionTime) {
208
+ this.cmi.setStartTime();
209
+ }
210
+
211
+ this.currentState = global_constants.STATE_INITIALIZED;
212
+ this.lastErrorCode = "0";
213
+ returnValue = global_constants.SCORM_TRUE;
214
+ this.processListeners(callbackName);
215
+ }
216
+
217
+ this.apiLog(
218
+ callbackName,
219
+ "returned: " + returnValue,
220
+ global_constants.LOG_LEVEL_INFO,
221
+ );
222
+ this.clearSCORMError(returnValue);
223
+
224
+ return returnValue;
225
+ }
226
+
227
+ abstract lmsInitialize(): string;
228
+
229
+ abstract lmsFinish(): string;
230
+
231
+ abstract lmsGetValue(CMIElement: string): string;
232
+
233
+ abstract lmsSetValue(CMIElement: string, value: any): string;
234
+
235
+ abstract lmsCommit(): string;
236
+
237
+ abstract lmsGetLastError(): string;
238
+
239
+ abstract lmsGetErrorString(CMIErrorCode: string | number): string;
240
+
241
+ abstract lmsGetDiagnostic(CMIErrorCode: string | number): string;
242
+
243
+ /**
244
+ * Getter for _error_codes
245
+ * @return {ErrorCode}
246
+ */
247
+ get error_codes(): ErrorCode {
248
+ return this._error_codes;
249
+ }
250
+
251
+ /**
252
+ * Getter for _settings
253
+ * @return {Settings}
254
+ */
255
+ get settings(): Settings {
256
+ return this._settings;
257
+ }
258
+
259
+ /**
260
+ * Setter for _settings
261
+ * @param {Settings} settings
262
+ */
263
+ set settings(settings: Settings) {
264
+ this._settings = { ...this._settings, ...settings };
265
+ }
266
+
267
+ /**
268
+ * Terminates the current run of the API
269
+ * @param {string} callbackName
270
+ * @param {boolean} checkTerminated
271
+ * @return {string}
272
+ */
273
+ terminate(callbackName: string, checkTerminated: boolean): string {
274
+ let returnValue = global_constants.SCORM_FALSE;
275
+
276
+ if (
277
+ this.checkState(
278
+ checkTerminated,
279
+ this._error_codes.TERMINATION_BEFORE_INIT,
280
+ this._error_codes.MULTIPLE_TERMINATION,
281
+ )
282
+ ) {
283
+ this.currentState = global_constants.STATE_TERMINATED;
284
+
285
+ const result: ResultObject = this.storeData(true);
286
+ if (typeof result.errorCode !== "undefined" && result.errorCode > 0) {
287
+ this.throwSCORMError(result.errorCode);
288
+ }
289
+ returnValue =
290
+ typeof result !== "undefined" && result.result
291
+ ? result.result
292
+ : global_constants.SCORM_FALSE;
293
+
294
+ if (checkTerminated) this.lastErrorCode = "0";
295
+
296
+ returnValue = global_constants.SCORM_TRUE;
297
+ this.processListeners(callbackName);
298
+ }
299
+
300
+ this.apiLog(
301
+ callbackName,
302
+ "returned: " + returnValue,
303
+ global_constants.LOG_LEVEL_INFO,
304
+ );
305
+ this.clearSCORMError(returnValue);
306
+
307
+ return returnValue;
308
+ }
309
+
310
+ /**
311
+ * Get the value of the CMIElement.
312
+ *
313
+ * @param {string} callbackName
314
+ * @param {boolean} checkTerminated
315
+ * @param {string} CMIElement
316
+ * @return {string}
317
+ */
318
+ getValue(
319
+ callbackName: string,
320
+ checkTerminated: boolean,
321
+ CMIElement: string,
322
+ ): string {
323
+ let returnValue: string = "";
324
+
325
+ if (
326
+ this.checkState(
327
+ checkTerminated,
328
+ this._error_codes.RETRIEVE_BEFORE_INIT,
329
+ this._error_codes.RETRIEVE_AFTER_TERM,
330
+ )
331
+ ) {
332
+ if (checkTerminated) this.lastErrorCode = "0";
333
+ try {
334
+ returnValue = this.getCMIValue(CMIElement);
335
+ } catch (e) {
336
+ returnValue = this.handleValueAccessException(e, returnValue);
337
+ }
338
+ this.processListeners(callbackName, CMIElement);
339
+ }
340
+
341
+ this.apiLog(
342
+ callbackName,
343
+ ": returned: " + returnValue,
344
+ global_constants.LOG_LEVEL_INFO,
345
+ CMIElement,
346
+ );
347
+ this.clearSCORMError(returnValue);
348
+
349
+ return returnValue;
350
+ }
351
+
352
+ /**
353
+ * Sets the value of the CMIElement.
354
+ *
355
+ * @param {string} callbackName
356
+ * @param {string} commitCallback
357
+ * @param {boolean} checkTerminated
358
+ * @param {string} CMIElement
359
+ * @param {*} value
360
+ * @return {string}
361
+ */
362
+ setValue(
363
+ callbackName: string,
364
+ commitCallback: string,
365
+ checkTerminated: boolean,
366
+ CMIElement: string,
367
+ value: any,
368
+ ): string {
369
+ if (value !== undefined) {
370
+ value = String(value);
371
+ }
372
+ let returnValue: string = global_constants.SCORM_FALSE;
373
+
374
+ if (
375
+ this.checkState(
376
+ checkTerminated,
377
+ this._error_codes.STORE_BEFORE_INIT,
378
+ this._error_codes.STORE_AFTER_TERM,
379
+ )
380
+ ) {
381
+ if (checkTerminated) this.lastErrorCode = "0";
382
+ try {
383
+ returnValue = this.setCMIValue(CMIElement, value);
384
+ } catch (e) {
385
+ this.handleValueAccessException(e, returnValue);
386
+ }
387
+ this.processListeners(callbackName, CMIElement, value);
388
+ }
389
+
390
+ if (returnValue === undefined) {
391
+ returnValue = global_constants.SCORM_FALSE;
392
+ }
393
+
394
+ // If we didn't have any errors while setting the data, go ahead and
395
+ // schedule a commit, if autocommit is turned on
396
+ if (String(this.lastErrorCode) === "0") {
397
+ if (this.settings.autocommit && !this._timeout) {
398
+ this.scheduleCommit(
399
+ this.settings.autocommitSeconds * 1000,
400
+ commitCallback,
401
+ );
402
+ }
403
+ }
404
+
405
+ this.apiLog(
406
+ callbackName,
407
+ ": " + value + ": result: " + returnValue,
408
+ global_constants.LOG_LEVEL_INFO,
409
+ CMIElement,
410
+ );
411
+ this.clearSCORMError(returnValue);
412
+
413
+ return returnValue;
414
+ }
415
+
416
+ /**
417
+ * Orders LMS to store all content parameters
418
+ * @param {string} callbackName
419
+ * @param {boolean} checkTerminated
420
+ * @return {string}
421
+ */
422
+ commit(callbackName: string, checkTerminated: boolean = false): string {
423
+ this.clearScheduledCommit();
424
+
425
+ let returnValue = global_constants.SCORM_FALSE;
426
+
427
+ if (
428
+ this.checkState(
429
+ checkTerminated,
430
+ this._error_codes.COMMIT_BEFORE_INIT,
431
+ this._error_codes.COMMIT_AFTER_TERM,
432
+ )
433
+ ) {
434
+ const result = this.storeData(false);
435
+ if (result.errorCode && result.errorCode > 0) {
436
+ this.throwSCORMError(result.errorCode);
437
+ }
438
+ returnValue =
439
+ typeof result !== "undefined" && result.result
440
+ ? result.result
441
+ : global_constants.SCORM_FALSE;
442
+
443
+ this.apiLog(
444
+ callbackName,
445
+ " Result: " + returnValue,
446
+ global_constants.LOG_LEVEL_DEBUG,
447
+ "HttpRequest",
448
+ );
449
+
450
+ if (checkTerminated) this.lastErrorCode = "0";
451
+
452
+ this.processListeners(callbackName);
453
+ }
454
+
455
+ this.apiLog(
456
+ callbackName,
457
+ "returned: " + returnValue,
458
+ global_constants.LOG_LEVEL_INFO,
459
+ );
460
+ this.clearSCORMError(returnValue);
461
+
462
+ return returnValue;
463
+ }
464
+
465
+ /**
466
+ * Returns last error code
467
+ * @param {string} callbackName
468
+ * @return {string}
469
+ */
470
+ getLastError(callbackName: string): string {
471
+ const returnValue = String(this.lastErrorCode);
472
+
473
+ this.processListeners(callbackName);
474
+
475
+ this.apiLog(
476
+ callbackName,
477
+ "returned: " + returnValue,
478
+ global_constants.LOG_LEVEL_INFO,
479
+ );
480
+
481
+ return returnValue;
482
+ }
483
+
484
+ /**
485
+ * Returns the errorNumber error description
486
+ *
487
+ * @param {string} callbackName
488
+ * @param {(string|number)} CMIErrorCode
489
+ * @return {string}
490
+ */
491
+ getErrorString(callbackName: string, CMIErrorCode: string | number): string {
492
+ let returnValue = "";
493
+
494
+ if (CMIErrorCode !== null && CMIErrorCode !== "") {
495
+ returnValue = this.getLmsErrorMessageDetails(CMIErrorCode);
496
+ this.processListeners(callbackName);
497
+ }
498
+
499
+ this.apiLog(
500
+ callbackName,
501
+ "returned: " + returnValue,
502
+ global_constants.LOG_LEVEL_INFO,
503
+ );
504
+
505
+ return returnValue;
506
+ }
507
+
508
+ /**
509
+ * Returns a comprehensive description of the errorNumber error.
510
+ *
511
+ * @param {string} callbackName
512
+ * @param {(string|number)} CMIErrorCode
513
+ * @return {string}
514
+ */
515
+ getDiagnostic(callbackName: string, CMIErrorCode: string | number): string {
516
+ let returnValue = "";
517
+
518
+ if (CMIErrorCode !== null && CMIErrorCode !== "") {
519
+ returnValue = this.getLmsErrorMessageDetails(CMIErrorCode, true);
520
+ this.processListeners(callbackName);
521
+ }
522
+
523
+ this.apiLog(
524
+ callbackName,
525
+ "returned: " + returnValue,
526
+ global_constants.LOG_LEVEL_INFO,
527
+ );
528
+
529
+ return returnValue;
530
+ }
531
+
532
+ /**
533
+ * Checks the LMS state and ensures it has been initialized.
534
+ *
535
+ * @param {boolean} checkTerminated
536
+ * @param {number} beforeInitError
537
+ * @param {number} afterTermError
538
+ * @return {boolean}
539
+ */
540
+ checkState(
541
+ checkTerminated: boolean,
542
+ beforeInitError: number,
543
+ afterTermError: number,
544
+ ): boolean {
545
+ if (this.isNotInitialized()) {
546
+ this.throwSCORMError(beforeInitError);
547
+ return false;
548
+ } else if (checkTerminated && this.isTerminated()) {
549
+ this.throwSCORMError(afterTermError);
550
+ return false;
551
+ }
552
+
553
+ return true;
554
+ }
555
+
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
+ /**
655
+ * Returns the message that corresponds to errorNumber
656
+ * APIs that inherit BaseAPI should override this function
657
+ *
658
+ * @param {(string|number)} _errorNumber
659
+ * @param {boolean} _detail
660
+ * @return {string}
661
+ * @abstract
662
+ */
663
+ getLmsErrorMessageDetails(
664
+ _errorNumber: string | number,
665
+ _detail: boolean = false,
666
+ ): string {
667
+ throw new Error(
668
+ "The getLmsErrorMessageDetails method has not been implemented",
669
+ );
670
+ }
671
+
672
+ /**
673
+ * Gets the value for the specific element.
674
+ * APIs that inherit BaseAPI should override this function
675
+ *
676
+ * @param {string} _CMIElement
677
+ * @return {string}
678
+ * @abstract
679
+ */
680
+ getCMIValue(_CMIElement: string): string {
681
+ throw new Error("The getCMIValue method has not been implemented");
682
+ }
683
+
684
+ /**
685
+ * Sets the value for the specific element.
686
+ * APIs that inherit BaseAPI should override this function
687
+ *
688
+ * @param {string} _CMIElement
689
+ * @param {any} _value
690
+ * @return {string}
691
+ * @abstract
692
+ */
693
+ setCMIValue(_CMIElement: string, _value: any): string {
694
+ throw new Error("The setCMIValue method has not been implemented");
695
+ }
696
+
697
+ /**
698
+ * Shared API method to set a valid for a given element.
699
+ *
700
+ * @param {string} methodName
701
+ * @param {boolean} scorm2004
702
+ * @param {string} CMIElement
703
+ * @param {any} value
704
+ * @return {string}
705
+ */
706
+ _commonSetCMIValue(
707
+ methodName: string,
708
+ scorm2004: boolean,
709
+ CMIElement: string,
710
+ value: any,
711
+ ): string {
712
+ if (!CMIElement || CMIElement === "") {
713
+ return global_constants.SCORM_FALSE;
714
+ }
715
+
716
+ const structure = CMIElement.split(".");
717
+ let refObject: RefObject = this;
718
+ let returnValue = global_constants.SCORM_FALSE;
719
+ let foundFirstIndex = false;
720
+
721
+ const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
722
+ const invalidErrorCode = scorm2004
723
+ ? this._error_codes.UNDEFINED_DATA_MODEL
724
+ : this._error_codes.GENERAL;
725
+
726
+ for (let idx = 0; idx < structure.length; idx++) {
727
+ const attribute = structure[idx];
728
+
729
+ 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);
736
+ } else if (!this._checkObjectHasProperty(refObject, attribute)) {
737
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
738
+ } else {
739
+ if (
740
+ this.isInitialized() &&
741
+ this.stringMatches(CMIElement, "\\.correct_responses\\.\\d+")
742
+ ) {
743
+ this.validateCorrectResponse(CMIElement, value);
744
+ }
745
+
746
+ if (!scorm2004 || this.lastErrorCode === "0") {
747
+ refObject[attribute] = value;
748
+ returnValue = global_constants.SCORM_TRUE;
749
+ }
750
+ }
751
+ } else {
752
+ refObject = refObject[attribute];
753
+ if (!refObject) {
754
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
755
+ break;
756
+ }
757
+
758
+ if (refObject instanceof CMIArray) {
759
+ const index = parseInt(structure[idx + 1], 10);
760
+
761
+ // SCO is trying to set an item on an array
762
+ if (!isNaN(index)) {
763
+ const item = refObject.childArray[index];
764
+
765
+ if (item) {
766
+ refObject = item;
767
+ foundFirstIndex = true;
768
+ } else {
769
+ const newChild = this.getChildElement(
770
+ CMIElement,
771
+ value,
772
+ foundFirstIndex,
773
+ );
774
+ foundFirstIndex = true;
775
+
776
+ if (!newChild) {
777
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
778
+ } else {
779
+ if (refObject.initialized) newChild.initialize();
780
+
781
+ refObject.childArray.push(newChild);
782
+ refObject = newChild;
783
+ }
784
+ }
785
+
786
+ // Have to update idx value to skip the array position
787
+ idx++;
788
+ }
789
+ }
790
+ }
791
+ }
792
+
793
+ if (returnValue === global_constants.SCORM_FALSE) {
794
+ this.apiLog(
795
+ methodName,
796
+ `There was an error setting the value for: ${CMIElement}, value of: ${value}`,
797
+ global_constants.LOG_LEVEL_WARNING,
798
+ );
799
+ }
800
+
801
+ return returnValue;
802
+ }
803
+
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
+ /**
829
+ * Gets a value from the CMI Object
830
+ *
831
+ * @param {string} methodName
832
+ * @param {boolean} scorm2004
833
+ * @param {string} CMIElement
834
+ * @return {any}
835
+ */
836
+ _commonGetCMIValue(
837
+ methodName: string,
838
+ scorm2004: boolean,
839
+ CMIElement: string,
840
+ ): any {
841
+ if (!CMIElement || CMIElement === "") {
842
+ return "";
843
+ }
844
+
845
+ const structure = CMIElement.split(".");
846
+ let refObject: RefObject = this;
847
+ let attribute = null;
848
+
849
+ const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
850
+ const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
851
+ const invalidErrorCode = scorm2004
852
+ ? this._error_codes.UNDEFINED_DATA_MODEL
853
+ : this._error_codes.GENERAL;
854
+
855
+ for (let idx = 0; idx < structure.length; idx++) {
856
+ attribute = structure[idx];
857
+
858
+ if (!scorm2004) {
859
+ if (idx === structure.length - 1) {
860
+ if (!this._checkObjectHasProperty(refObject, attribute)) {
861
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
862
+ return;
863
+ }
864
+ }
865
+ } else {
866
+ if (
867
+ String(attribute).substring(0, 8) === "{target=" &&
868
+ typeof refObject._isTargetValid == "function"
869
+ ) {
870
+ const target = String(attribute).substring(
871
+ 8,
872
+ String(attribute).length - 9,
873
+ );
874
+ return refObject._isTargetValid(target);
875
+ } else if (!this._checkObjectHasProperty(refObject, attribute)) {
876
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
877
+ return;
878
+ }
879
+ }
880
+
881
+ refObject = refObject[attribute];
882
+ if (refObject === undefined) {
883
+ this.throwSCORMError(invalidErrorCode, invalidErrorMessage);
884
+ break;
885
+ }
886
+
887
+ if (refObject instanceof CMIArray) {
888
+ const index = parseInt(structure[idx + 1], 10);
889
+
890
+ // SCO is trying to set an item on an array
891
+ if (!isNaN(index)) {
892
+ const item = refObject.childArray[index];
893
+
894
+ if (item) {
895
+ refObject = item;
896
+ } else {
897
+ this.throwSCORMError(
898
+ this._error_codes.VALUE_NOT_INITIALIZED,
899
+ uninitializedErrorMessage,
900
+ );
901
+ break;
902
+ }
903
+
904
+ // Have to update idx value to skip the array position
905
+ idx++;
906
+ }
907
+ }
908
+ }
909
+
910
+ if (refObject === null || refObject === undefined) {
911
+ if (!scorm2004) {
912
+ if (attribute === "_children") {
913
+ this.throwSCORMError(scorm12_error_codes.CHILDREN_ERROR);
914
+ } else if (attribute === "_count") {
915
+ this.throwSCORMError(scorm12_error_codes.COUNT_ERROR);
916
+ }
917
+ }
918
+ } else {
919
+ return refObject;
920
+ }
921
+ }
922
+
923
+ /**
924
+ * Returns true if the API's current state is STATE_INITIALIZED
925
+ *
926
+ * @return {boolean}
927
+ */
928
+ isInitialized(): boolean {
929
+ return this.currentState === global_constants.STATE_INITIALIZED;
930
+ }
931
+
932
+ /**
933
+ * Returns true if the API's current state is STATE_NOT_INITIALIZED
934
+ *
935
+ * @return {boolean}
936
+ */
937
+ isNotInitialized(): boolean {
938
+ return this.currentState === global_constants.STATE_NOT_INITIALIZED;
939
+ }
940
+
941
+ /**
942
+ * Returns true if the API's current state is STATE_TERMINATED
943
+ *
944
+ * @return {boolean}
945
+ */
946
+ isTerminated(): boolean {
947
+ return this.currentState === global_constants.STATE_TERMINATED;
948
+ }
949
+
950
+ /**
951
+ * Provides a mechanism for attaching to a specific SCORM event
952
+ *
953
+ * @param {string} listenerName
954
+ * @param {function} callback
955
+ */
956
+ on(listenerName: string, callback: Function) {
957
+ if (!callback) return;
958
+
959
+ const listenerFunctions = listenerName.split(" ");
960
+ for (let i = 0; i < listenerFunctions.length; i++) {
961
+ const listenerSplit = listenerFunctions[i].split(".");
962
+ if (listenerSplit.length === 0) return;
963
+
964
+ const functionName = listenerSplit[0];
965
+
966
+ let CMIElement = null;
967
+ if (listenerSplit.length > 1) {
968
+ CMIElement = listenerName.replace(functionName + ".", "");
969
+ }
970
+
971
+ this.listenerArray.push({
972
+ functionName: functionName,
973
+ CMIElement: CMIElement,
974
+ callback: callback,
975
+ });
976
+
977
+ this.apiLog(
978
+ "on",
979
+ `Added event listener: ${this.listenerArray.length}`,
980
+ global_constants.LOG_LEVEL_INFO,
981
+ functionName,
982
+ );
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Provides a mechanism for detaching a specific SCORM event listener
988
+ *
989
+ * @param {string} listenerName
990
+ * @param {function} callback
991
+ */
992
+ off(listenerName: string, callback: Function) {
993
+ if (!callback) return;
994
+
995
+ const listenerFunctions = listenerName.split(" ");
996
+ for (let i = 0; i < listenerFunctions.length; i++) {
997
+ const listenerSplit = listenerFunctions[i].split(".");
998
+ if (listenerSplit.length === 0) return;
999
+
1000
+ const functionName = listenerSplit[0];
1001
+
1002
+ let CMIElement = null;
1003
+ if (listenerSplit.length > 1) {
1004
+ CMIElement = listenerName.replace(functionName + ".", "");
1005
+ }
1006
+
1007
+ const removeIndex = this.listenerArray.findIndex(
1008
+ (obj) =>
1009
+ obj.functionName === functionName &&
1010
+ obj.CMIElement === CMIElement &&
1011
+ obj.callback === callback,
1012
+ );
1013
+ if (removeIndex !== -1) {
1014
+ this.listenerArray.splice(removeIndex, 1);
1015
+ this.apiLog(
1016
+ "off",
1017
+ `Removed event listener: ${this.listenerArray.length}`,
1018
+ global_constants.LOG_LEVEL_INFO,
1019
+ functionName,
1020
+ );
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Provides a mechanism for clearing all listeners from a specific SCORM event
1027
+ *
1028
+ * @param {string} listenerName
1029
+ */
1030
+ clear(listenerName: string) {
1031
+ const listenerFunctions = listenerName.split(" ");
1032
+ for (let i = 0; i < listenerFunctions.length; i++) {
1033
+ const listenerSplit = listenerFunctions[i].split(".");
1034
+ if (listenerSplit.length === 0) return;
1035
+
1036
+ const functionName = listenerSplit[0];
1037
+
1038
+ let CMIElement = null;
1039
+ if (listenerSplit.length > 1) {
1040
+ CMIElement = listenerName.replace(functionName + ".", "");
1041
+ }
1042
+
1043
+ this.listenerArray = this.listenerArray.filter(
1044
+ (obj) =>
1045
+ obj.functionName !== functionName && obj.CMIElement !== CMIElement,
1046
+ );
1047
+ }
1048
+ }
1049
+
1050
+ /**
1051
+ * Processes any 'on' listeners that have been created
1052
+ *
1053
+ * @param {string} functionName
1054
+ * @param {string} CMIElement
1055
+ * @param {any} value
1056
+ */
1057
+ processListeners(functionName: string, CMIElement?: string, value?: any) {
1058
+ this.apiLog(
1059
+ functionName,
1060
+ value,
1061
+ global_constants.LOG_LEVEL_INFO,
1062
+ CMIElement,
1063
+ );
1064
+ for (let i = 0; i < this.listenerArray.length; i++) {
1065
+ const listener = this.listenerArray[i];
1066
+ const functionsMatch = listener.functionName === functionName;
1067
+ const listenerHasCMIElement = !!listener.CMIElement;
1068
+ let CMIElementsMatch = false;
1069
+ if (
1070
+ CMIElement &&
1071
+ listener.CMIElement &&
1072
+ listener.CMIElement.substring(listener.CMIElement.length - 1) === "*"
1073
+ ) {
1074
+ CMIElementsMatch =
1075
+ CMIElement.indexOf(
1076
+ listener.CMIElement.substring(0, listener.CMIElement.length - 1),
1077
+ ) === 0;
1078
+ } else {
1079
+ CMIElementsMatch = listener.CMIElement === CMIElement;
1080
+ }
1081
+
1082
+ if (functionsMatch && (!listenerHasCMIElement || CMIElementsMatch)) {
1083
+ this.apiLog(
1084
+ "processListeners",
1085
+ `Processing listener: ${listener.functionName}`,
1086
+ global_constants.LOG_LEVEL_INFO,
1087
+ CMIElement,
1088
+ );
1089
+ listener.callback(CMIElement, value);
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Throws a SCORM error
1096
+ *
1097
+ * @param {number} errorNumber
1098
+ * @param {string} message
1099
+ */
1100
+ throwSCORMError(errorNumber: number, message?: string) {
1101
+ if (!message) {
1102
+ message = this.getLmsErrorMessageDetails(errorNumber);
1103
+ }
1104
+
1105
+ this.apiLog(
1106
+ "throwSCORMError",
1107
+ errorNumber + ": " + message,
1108
+ global_constants.LOG_LEVEL_ERROR,
1109
+ );
1110
+
1111
+ this.lastErrorCode = String(errorNumber);
1112
+ }
1113
+
1114
+ /**
1115
+ * Clears the last SCORM error code on success.
1116
+ *
1117
+ * @param {string} success
1118
+ */
1119
+ clearSCORMError(success: string) {
1120
+ if (success !== undefined && success !== global_constants.SCORM_FALSE) {
1121
+ this.lastErrorCode = "0";
1122
+ }
1123
+ }
1124
+
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
+ /**
1136
+ * Load the CMI from a flattened JSON object
1137
+ * @param {RefObject} json
1138
+ * @param {string} CMIElement
1139
+ */
1140
+ loadFromFlattenedJSON(json: RefObject, CMIElement: string) {
1141
+ if (!this.isNotInitialized()) {
1142
+ console.error(
1143
+ "loadFromFlattenedJSON can only be called before the call to lmsInitialize.",
1144
+ );
1145
+ return;
1146
+ }
1147
+
1148
+ /**
1149
+ * Test match pattern.
1150
+ *
1151
+ * @param {string} a
1152
+ * @param {string} c
1153
+ * @param {RegExp} a_pattern
1154
+ * @return {number}
1155
+ */
1156
+ function testPattern(
1157
+ a: string,
1158
+ c: string,
1159
+ a_pattern: RegExp,
1160
+ ): number | null {
1161
+ const a_match = a.match(a_pattern);
1162
+
1163
+ let c_match;
1164
+ if (a_match !== null && (c_match = c.match(a_pattern)) !== null) {
1165
+ const a_num = Number(a_match[2]);
1166
+ const c_num = Number(c_match[2]);
1167
+ if (a_num === c_num) {
1168
+ if (a_match[3] === "id") {
1169
+ return -1;
1170
+ } else if (a_match[3] === "type") {
1171
+ if (c_match[3] === "id") {
1172
+ return 1;
1173
+ } else {
1174
+ return -1;
1175
+ }
1176
+ } else {
1177
+ return 1;
1178
+ }
1179
+ }
1180
+ return a_num - c_num;
1181
+ }
1182
+
1183
+ return null;
1184
+ }
1185
+
1186
+ const int_pattern = /^(cmi\.interactions\.)(\d+)\.(.*)$/;
1187
+ const obj_pattern = /^(cmi\.objectives\.)(\d+)\.(.*)$/;
1188
+
1189
+ const result = Object.keys(json).map(function (key) {
1190
+ return [String(key), json[key]];
1191
+ });
1192
+
1193
+ // CMI interactions need to have id and type loaded before any other fields
1194
+ result.sort(function ([a, _b], [c, _d]) {
1195
+ let test;
1196
+ if ((test = testPattern(a, c, int_pattern)) !== null) {
1197
+ return test;
1198
+ }
1199
+ if ((test = testPattern(a, c, obj_pattern)) !== null) {
1200
+ return test;
1201
+ }
1202
+
1203
+ if (a < c) {
1204
+ return -1;
1205
+ }
1206
+ if (a > c) {
1207
+ return 1;
1208
+ }
1209
+ return 0;
1210
+ });
1211
+
1212
+ let obj: RefObject;
1213
+ result.forEach((element) => {
1214
+ obj = {};
1215
+ obj[element[0]] = element[1];
1216
+ this.loadFromJSON(unflatten(obj), CMIElement);
1217
+ });
1218
+ }
1219
+
1220
+ /**
1221
+ * Loads CMI data from a JSON object.
1222
+ *
1223
+ * @param {RefObject} json
1224
+ * @param {string} CMIElement
1225
+ */
1226
+ loadFromJSON(json: RefObject, CMIElement: string) {
1227
+ if (!this.isNotInitialized()) {
1228
+ console.error(
1229
+ "loadFromJSON can only be called before the call to lmsInitialize.",
1230
+ );
1231
+ return;
1232
+ }
1233
+
1234
+ CMIElement = CMIElement !== undefined ? CMIElement : "cmi";
1235
+
1236
+ this.startingData = json;
1237
+
1238
+ // could this be refactored down to flatten(json) then setCMIValue on each?
1239
+ for (const key in json) {
1240
+ if ({}.hasOwnProperty.call(json, key) && json[key]) {
1241
+ const currentCMIElement = (CMIElement ? CMIElement + "." : "") + key;
1242
+ const value = json[key];
1243
+
1244
+ if (value["childArray"]) {
1245
+ for (let i = 0; i < value["childArray"].length; i++) {
1246
+ this.loadFromJSON(
1247
+ value["childArray"][i],
1248
+ currentCMIElement + "." + i,
1249
+ );
1250
+ }
1251
+ } else if (value.constructor === Object) {
1252
+ this.loadFromJSON(value, currentCMIElement);
1253
+ } else {
1254
+ this.setCMIValue(currentCMIElement, value);
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Render the CMI object to JSON for sending to an LMS.
1262
+ *
1263
+ * @return {string}
1264
+ */
1265
+ renderCMIToJSONString(): string {
1266
+ const cmi = this.cmi;
1267
+ // 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 });
1270
+ }
1271
+
1272
+ /**
1273
+ * Returns a JS object representing the current cmi
1274
+ * @return {object}
1275
+ */
1276
+ 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
+ return JSON.parse(this.renderCMIToJSONString());
1280
+ }
1281
+
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
+ /**
1293
+ * Send the request to the LMS
1294
+ * @param {string} url
1295
+ * @param {RefObject|Array} params
1296
+ * @param {boolean} immediate
1297
+ * @return {ResultObject}
1298
+ */
1299
+ processHttpRequest(
1300
+ url: string,
1301
+ params: RefObject | Array<any>,
1302
+ immediate: boolean = false,
1303
+ ): ResultObject {
1304
+ const api = this;
1305
+ const genericError: ResultObject = {
1306
+ result: global_constants.SCORM_FALSE,
1307
+ errorCode: this.error_codes.GENERAL,
1308
+ };
1309
+
1310
+ const process = async (
1311
+ url: string,
1312
+ params: RefObject | Array<any>,
1313
+ settings: Settings,
1314
+ ): Promise<ResultObject> => {
1315
+ try {
1316
+ 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
+ }
1344
+
1345
+ return result;
1346
+ } catch (e) {
1347
+ this.apiLog("processHttpRequest", e, global_constants.LOG_LEVEL_ERROR);
1348
+ api.processListeners("CommitError");
1349
+ return genericError;
1350
+ }
1351
+ };
1352
+
1353
+ const debouncedProcess = debounce(process, 500, immediate);
1354
+ debouncedProcess(url, params, this.settings);
1355
+
1356
+ return {
1357
+ result: global_constants.SCORM_TRUE,
1358
+ errorCode: 0,
1359
+ };
1360
+ }
1361
+
1362
+ /**
1363
+ * Throws a SCORM error
1364
+ *
1365
+ * @param {number} when - the number of milliseconds to wait before committing
1366
+ * @param {string} callback - the name of the commit event callback
1367
+ */
1368
+ scheduleCommit(when: number, callback: string) {
1369
+ this._timeout = new ScheduledCommit(this, when, callback);
1370
+ this.apiLog(
1371
+ "scheduleCommit",
1372
+ "scheduled",
1373
+ global_constants.LOG_LEVEL_DEBUG,
1374
+ "",
1375
+ );
1376
+ }
1377
+
1378
+ /**
1379
+ * Clears and cancels any currently scheduled commits
1380
+ */
1381
+ clearScheduledCommit() {
1382
+ if (this._timeout) {
1383
+ this._timeout.cancel();
1384
+ this._timeout = undefined;
1385
+ this.apiLog(
1386
+ "clearScheduledCommit",
1387
+ "cleared",
1388
+ global_constants.LOG_LEVEL_DEBUG,
1389
+ "",
1390
+ );
1391
+ }
1392
+ }
1393
+
1394
+ private handleValueAccessException(e: any, returnValue: string) {
1395
+ if (e instanceof ValidationError) {
1396
+ this.lastErrorCode = String(e.errorCode);
1397
+ returnValue = global_constants.SCORM_FALSE;
1398
+ } else {
1399
+ if (e instanceof Error && e.message) {
1400
+ console.error(e.message);
1401
+ } else {
1402
+ console.error(e);
1403
+ }
1404
+ this.throwSCORMError(this._error_codes.GENERAL);
1405
+ }
1406
+ return returnValue;
1407
+ }
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
+
1419
+ /**
1420
+ * Constructor for ScheduledCommit
1421
+ * @param {BaseAPI} API
1422
+ * @param {number} when
1423
+ * @param {string} callback
1424
+ */
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;
1429
+ }
1430
+
1431
+ /**
1432
+ * Cancel any currently scheduled commit
1433
+ */
1434
+ cancel() {
1435
+ this._cancelled = true;
1436
+ if (this._timeout) {
1437
+ clearTimeout(this._timeout);
1438
+ }
1439
+ }
1440
+
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);
1447
+ }
1448
+ }
1449
+ }