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.
- package/.babelrc +18 -7
- package/.github/dependabot.yml +5 -0
- package/.github/workflows/main.yml +79 -0
- package/.jsdoc.json +4 -5
- package/.mocharc.json +8 -0
- package/.run/Mocha Unit Tests.run.xml +5 -2
- package/CONTRIBUTING.md +1 -1
- package/README.md +14 -1
- package/dist/aicc.js +3661 -7170
- package/dist/aicc.js.map +1 -1
- package/dist/aicc.min.js +2 -40
- package/dist/aicc.min.js.map +1 -0
- package/dist/scorm-again.js +5671 -10695
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +2 -52
- package/dist/scorm-again.min.js.map +1 -0
- package/dist/scorm12.js +2871 -5433
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +2 -34
- package/dist/scorm12.min.js.map +1 -0
- package/dist/scorm2004.js +3868 -6797
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +2 -40
- package/dist/scorm2004.min.js.map +1 -0
- package/eslint.config.js +21 -0
- package/package.json +72 -34
- package/results.json +34254 -0
- package/src/{AICC.js → AICC.ts} +27 -21
- package/src/BaseAPI.ts +1449 -0
- package/src/Scorm12API.ts +360 -0
- package/src/{Scorm2004API.js → Scorm2004API.ts} +245 -163
- package/src/cmi/aicc_cmi.ts +1248 -0
- package/src/cmi/common.ts +411 -0
- package/src/cmi/scorm12_cmi.ts +1426 -0
- package/src/cmi/scorm2004_cmi.ts +1874 -0
- package/src/constants/api_constants.ts +318 -0
- package/src/constants/error_codes.ts +88 -0
- package/src/constants/language_constants.ts +394 -0
- package/src/constants/regex.ts +97 -0
- package/src/constants/{response_constants.js → response_constants.ts} +67 -62
- package/src/exceptions.ts +133 -0
- package/src/exports/aicc.js +1 -1
- package/src/exports/scorm-again.js +3 -3
- package/src/exports/scorm12.js +1 -1
- package/src/exports/scorm2004.js +1 -1
- package/src/{utilities.js → utilities.ts} +114 -74
- package/tea.yaml +6 -0
- package/test/{AICC.spec.js → AICC.spec.ts} +70 -72
- package/test/Scorm12API.spec.ts +580 -0
- package/test/Scorm2004API.spec.ts +812 -0
- package/test/api_helpers.ts +176 -0
- package/test/cmi/{aicc_cmi.spec.js → aicc_cmi.spec.ts} +193 -209
- package/test/cmi/{scorm12_cmi.spec.js → scorm12_cmi.spec.ts} +251 -269
- package/test/cmi/scorm2004_cmi.spec.ts +1031 -0
- package/test/cmi_helpers.ts +207 -0
- package/test/exceptions.spec.ts +79 -0
- package/test/field_values.ts +202 -0
- package/test/utilities.spec.ts +322 -0
- package/tsconfig.json +18 -0
- package/webpack.config.js +65 -0
- package/.circleci/config.yml +0 -99
- package/.codeclimate.yml +0 -7
- package/.eslintrc.js +0 -36
- package/src/.flowconfig +0 -11
- package/src/BaseAPI.js +0 -1275
- package/src/Scorm12API.js +0 -308
- package/src/cmi/aicc_cmi.js +0 -1141
- package/src/cmi/common.js +0 -328
- package/src/cmi/scorm12_cmi.js +0 -1312
- package/src/cmi/scorm2004_cmi.js +0 -1692
- package/src/constants/api_constants.js +0 -218
- package/src/constants/error_codes.js +0 -87
- package/src/constants/language_constants.js +0 -76
- package/src/constants/regex.js +0 -84
- package/src/exceptions.js +0 -104
- package/test/Scorm12API.spec.js +0 -528
- package/test/Scorm2004API.spec.js +0 -775
- package/test/abstract_classes.spec.js +0 -17
- package/test/api_helpers.js +0 -128
- package/test/cmi/scorm2004_cmi.spec.js +0 -1066
- package/test/cmi_helpers.js +0 -161
- package/test/exceptions.spec.js +0 -71
- package/test/field_values.js +0 -353
- package/test/utilities.spec.js +0 -339
- 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
|
+
}
|