coursecode 0.1.14 → 0.1.15
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.
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { generateId } from '../utilities/utilities.js';
|
|
2
2
|
import { logger } from '../utilities/logger.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Safely serialize any value for logging. Handles circular references,
|
|
6
|
+
* Error instances, and oversized payloads without throwing.
|
|
7
|
+
*/
|
|
8
|
+
function safeStringify(data, maxLength = 4096) {
|
|
9
|
+
const seen = new WeakSet();
|
|
10
|
+
try {
|
|
11
|
+
const json = JSON.stringify(data, (key, value) => {
|
|
12
|
+
if (value instanceof Error) {
|
|
13
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'object' && value !== null) {
|
|
16
|
+
if (seen.has(value)) return '[Circular]';
|
|
17
|
+
seen.add(value);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}, 2);
|
|
21
|
+
if (json && json.length > maxLength) {
|
|
22
|
+
return json.slice(0, maxLength) + '...[truncated]';
|
|
23
|
+
}
|
|
24
|
+
return json;
|
|
25
|
+
} catch {
|
|
26
|
+
return `[Unserializable: ${typeof data}]`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
class EventBus {
|
|
5
31
|
constructor() {
|
|
6
32
|
// Event listeners registry
|
|
7
33
|
this.events = {};
|
|
34
|
+
// Re-entrancy guard — prevents infinite :error → log → :error cascade
|
|
35
|
+
this._emittingError = false;
|
|
8
36
|
}
|
|
9
37
|
|
|
10
38
|
/**
|
|
@@ -86,31 +114,50 @@ class EventBus {
|
|
|
86
114
|
return false;
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
if (event.endsWith(':error')) {
|
|
91
|
-
logger.error(`[EventBus Error] ${event}:`, JSON.stringify(data, null, 2));
|
|
92
|
-
}
|
|
117
|
+
const isErrorEvent = event.endsWith(':error');
|
|
93
118
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
// Re-entrancy guard — if we're already inside an :error emit,
|
|
120
|
+
// suppress to prevent infinite cascade
|
|
121
|
+
if (isErrorEvent) {
|
|
122
|
+
if (this._emittingError) {
|
|
123
|
+
console.warn(`[EventBus] Suppressed recursive error event: ${event}`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
this._emittingError = true;
|
|
127
|
+
}
|
|
97
128
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
try {
|
|
130
|
+
// Automatically log events that follow the ':error' naming convention
|
|
131
|
+
if (isErrorEvent) {
|
|
132
|
+
logger.error(`[EventBus Error] ${event}:`, safeStringify(data));
|
|
133
|
+
}
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
// Create a copy of listeners to avoid issues if listeners modify the array
|
|
136
|
+
const listeners = [...this.events[event]];
|
|
137
|
+
const onceListeners = [];
|
|
138
|
+
|
|
139
|
+
listeners.forEach(listener => {
|
|
140
|
+
try {
|
|
141
|
+
listener.callback(data);
|
|
142
|
+
|
|
143
|
+
// Track once listeners for removal
|
|
144
|
+
if (listener.once) {
|
|
145
|
+
onceListeners.push(listener.id);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Log the error but don't break other listeners — use safeStringify
|
|
149
|
+
// to prevent a secondary cascade from unserializable error objects
|
|
150
|
+
logger.error(`[EventBus] Error in listener for '${event}':`, safeStringify(error));
|
|
105
151
|
}
|
|
106
|
-
}
|
|
107
|
-
// Log the error but don't break other listeners
|
|
108
|
-
logger.error(`[EventBus] Error in listener for '${event}':`, error);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
152
|
+
});
|
|
111
153
|
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
// Remove once listeners
|
|
155
|
+
onceListeners.forEach(id => this.off(event, id));
|
|
156
|
+
} finally {
|
|
157
|
+
if (isErrorEvent) {
|
|
158
|
+
this._emittingError = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
114
161
|
|
|
115
162
|
return true;
|
|
116
163
|
}
|
|
@@ -561,13 +561,27 @@ export class Scorm2004Driver extends ScormDriverBase {
|
|
|
561
561
|
* Populates the CMI cache at init time. Single LMS read pass.
|
|
562
562
|
*/
|
|
563
563
|
_populateCache() {
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
564
|
+
// Helper: read a CMI value via strict _getValue, but tolerate error 403
|
|
565
|
+
// ("Data Model Element Value Not Initialized") which strict LMSes like
|
|
566
|
+
// SCORM Cloud return for unset elements on a fresh session.
|
|
567
|
+
// Any other SCORM error still throws through _getValue's normal path.
|
|
568
|
+
const getOrDefault = (key, fallback) => {
|
|
569
|
+
try {
|
|
570
|
+
return this._getValue(key) || fallback;
|
|
571
|
+
} catch (e) {
|
|
572
|
+
const code = this._scorm.debug.getCode();
|
|
573
|
+
if (code === 403) return fallback;
|
|
574
|
+
throw e;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// Read-only scalars (may be uninitialized on first launch)
|
|
579
|
+
this._cmiCache.entry = getOrDefault('cmi.entry', '');
|
|
580
|
+
this._cmiCache.bookmark = getOrDefault('cmi.location', '');
|
|
581
|
+
this._cmiCache.completionStatus = getOrDefault('cmi.completion_status', 'unknown');
|
|
582
|
+
this._cmiCache.successStatus = getOrDefault('cmi.success_status', 'unknown');
|
|
583
|
+
this._cmiCache.learnerId = getOrDefault('cmi.learner_id', '');
|
|
584
|
+
this._cmiCache.learnerName = getOrDefault('cmi.learner_name', '');
|
|
571
585
|
|
|
572
586
|
// Skip array hydration for fresh sessions
|
|
573
587
|
if (this._cmiCache.entry === 'ab-initio') {
|