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
- // Automatically log events that follow the ':error' naming convention
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
- // Create a copy of listeners to avoid issues if listeners modify the array
95
- const listeners = [...this.events[event]];
96
- const onceListeners = [];
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
- listeners.forEach(listener => {
99
- try {
100
- listener.callback(data);
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
- // Track once listeners for removal
103
- if (listener.once) {
104
- onceListeners.push(listener.id);
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
- } catch (error) {
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
- // Remove once listeners
113
- onceListeners.forEach(id => this.off(event, id));
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
- // Read-only scalars
565
- this._cmiCache.entry = this._getValue('cmi.entry') || '';
566
- this._cmiCache.bookmark = this._getValue('cmi.location') || '';
567
- this._cmiCache.completionStatus = this._getValue('cmi.completion_status') || 'unknown';
568
- this._cmiCache.successStatus = this._getValue('cmi.success_status') || 'unknown';
569
- this._cmiCache.learnerId = this._getValue('cmi.learner_id') || '';
570
- this._cmiCache.learnerName = this._getValue('cmi.learner_name') || '';
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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {