adapt-authoring-core 1.4.3 → 1.6.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.
@@ -7,6 +7,12 @@
7
7
  "type": "boolean",
8
8
  "default": true
9
9
  },
10
+ "moduleLoadTimeout": {
11
+ "description": "Amount of time to wait for a module to load before erroring",
12
+ "type": "string",
13
+ "isTimeMs": true,
14
+ "default": "1m"
15
+ },
10
16
  "dataDir": {
11
17
  "description": "Directory for persistant application data",
12
18
  "type": "string",
@@ -18,7 +18,7 @@ export default class Contributors {
18
18
  this.tiers = [
19
19
  { name: 'gold', count: 3, border: '#FFD700' },
20
20
  { name: 'silver', count: 6, border: '#C0C0C0' },
21
- { name: 'bronze', count: 10, border: '#CD7F32' },
21
+ { name: 'bronze', count: 9, border: '#CD7F32' },
22
22
  { name: 'contributor' }
23
23
  ]
24
24
  }
@@ -71,13 +71,12 @@ class AbstractModule {
71
71
  if (this._isReady) {
72
72
  return
73
73
  }
74
- await this.readyHook.invoke(error)
75
- if (error) {
76
- return
74
+ if (!error) {
75
+ this._isReady = true
77
76
  }
78
- this._isReady = true
79
77
  this.initTime = Math.round((Date.now() - this._startTime))
80
78
  this.log('verbose', AbstractModule.MODULE_READY, this.initTime)
79
+ await this.readyHook.invoke(error)
81
80
  }
82
81
 
83
82
  /**
package/lib/App.js CHANGED
@@ -70,7 +70,9 @@ class App extends AbstractModule {
70
70
  if (failedMods.length) this.log('warn', `${failedMods.length} module${failedMods.length === 1 ? '' : 's'} failed to load: ${failedMods}. See above for details`)
71
71
  if (startError) {
72
72
  process.exitCode = 1
73
- throw new Error('Failed to start App')
73
+ const e = new Error('Failed to start App')
74
+ e.cause = startError
75
+ throw e
74
76
  }
75
77
  }
76
78
 
@@ -11,14 +11,15 @@ import Utils from './Utils.js'
11
11
  */
12
12
  class DependencyLoader {
13
13
  /**
14
- * @param {Object} app The main app instance
14
+ * Creates a new DependencyLoader instance
15
+ * @param {App} app The main app instance
15
16
  */
16
17
  constructor (app) {
17
18
  /**
18
- * Name of the class (onvenience function to stay consistent with other classes)
19
- * @type {String}
19
+ * Name of the class (convenience function to stay consistent with other classes)
20
+ * @type {string}
20
21
  */
21
- this.name = this.constructor.name
22
+ this.name = this.constructor.name.toLowerCase()
22
23
  /**
23
24
  * Reference to the main app
24
25
  * @type {App}
@@ -26,22 +27,22 @@ class DependencyLoader {
26
27
  this.app = app
27
28
  /**
28
29
  * Key/value store of all the Adapt dependencies' configs. Note this includes dependencies which are not loaded as Adapt modules (i.e. `module: false`).
29
- * @type {Object}
30
+ * @type {Object<string, Object>}
30
31
  */
31
32
  this.configs = {}
32
33
  /**
33
- * List of dependency instances
34
- * @type {object}
34
+ * Map of module names to their loaded instances
35
+ * @type {Object<string, Object>}
35
36
  */
36
37
  this.instances = {}
37
38
  /**
38
- * Peer dependencies listed for each dependency
39
- * @type {object}
39
+ * Map of module names to arrays of modules that depend on them as peer dependencies
40
+ * @type {Object<string, Array<string>>}
40
41
  */
41
42
  this.peerDependencies = {}
42
43
  /**
43
- * List of modules which have failed to load
44
- * @type {Array}
44
+ * List of module names which have failed to load
45
+ * @type {Array<string>}
45
46
  */
46
47
  this.failedModules = []
47
48
  /**
@@ -54,11 +55,14 @@ class DependencyLoader {
54
55
  * @type {Hook}
55
56
  */
56
57
  this.moduleLoadedHook = new Hook()
58
+
59
+ this.moduleLoadedHook.tap(this.logProgress, this)
57
60
  }
58
61
 
59
62
  /**
60
- * Loads all Adapt module dependencies
61
- * @return {Promise}
63
+ * Loads all Adapt module dependencies. Essential modules are loaded first, then non-essential modules (with force mode).
64
+ * @return {Promise<void>}
65
+ * @throws {Error} When any essential module fails to load
62
66
  */
63
67
  async load () {
64
68
  await this.loadConfigs()
@@ -71,9 +75,7 @@ class DependencyLoader {
71
75
  }, { essential: [], theRest: [] })
72
76
  // load each set of deps
73
77
  await this.loadModules(essential)
74
- try {
75
- await this.loadModules(theRest)
76
- } catch (e) {} // not a problem if non-essential module fails to load
78
+ await this.loadModules(theRest, { force: true })
77
79
 
78
80
  if (this.failedModules.length) {
79
81
  throw new Error(`Failed to load modules ${this.failedModules.join(', ')}`)
@@ -81,8 +83,8 @@ class DependencyLoader {
81
83
  }
82
84
 
83
85
  /**
84
- * Loads configs for all dependencies
85
- * @return {Promise}
86
+ * Loads configuration files for all Adapt dependencies found in node_modules.
87
+ * @return {Promise<void>}
86
88
  */
87
89
  async loadConfigs () {
88
90
  /** @ignore */ this._configsLoaded = false
@@ -117,9 +119,9 @@ class DependencyLoader {
117
119
  }
118
120
 
119
121
  /**
120
- * Loads the relevant configuration files for an Adapt module
121
- * @param {String} modDir Module directory
122
- * @return {Promise}
122
+ * Loads the relevant configuration files for an Adapt module by reading and merging package.json and adapt.json
123
+ * @param {string} modDir Absolute path to the module directory
124
+ * @return {Promise<Object>} Resolves with configuration object
123
125
  */
124
126
  async loadModuleConfig (modDir) {
125
127
  return {
@@ -130,9 +132,10 @@ class DependencyLoader {
130
132
  }
131
133
 
132
134
  /**
133
- * Loads a single Adapt module. Should not need to be called directly.
134
- * @param {String} modName Name of the module to load
135
- * @return {Promise} Resolves with module instance on module.onReady
135
+ * Loads a single Adapt module by dynamically importing it, instantiating it, and waiting for its onReady promise. Should not need to be called directly.
136
+ * @param {string} modName Name of the module to load (e.g., 'adapt-authoring-core')
137
+ * @return {Promise<Object>} Resolves with module instance when module.onReady completes
138
+ * @throws {Error} When module already exists, is in an unknown format or cannot be initialised (or initialisation exceeds 60 second timeout)
136
139
  */
137
140
  async loadModule (modName) {
138
141
  if (this.instances[modName]) {
@@ -154,9 +157,11 @@ class DependencyLoader {
154
157
  throw new Error('Module must define onReady function')
155
158
  }
156
159
  try {
160
+ // all essential modules will use hard-coded value, as config won't be loaded yet
161
+ const timeout = this.getConfig('moduleLoadTimeout') ?? 10000
157
162
  await Promise.race([
158
163
  instance.onReady(),
159
- new Promise((resolve, reject) => setTimeout(() => reject(new Error(`${modName} load exceeded timeout (60000)`)), 60000))
164
+ new Promise((resolve, reject) => setTimeout(() => reject(new Error(`${modName} load exceeded timeout (${timeout})`)), timeout))
160
165
  ])
161
166
  this.instances[modName] = instance
162
167
  await this.moduleLoadedHook.invoke(null, instance)
@@ -169,14 +174,23 @@ class DependencyLoader {
169
174
 
170
175
  /**
171
176
  * Loads a list of Adapt modules. Should not need to be called directly.
172
- * @param {Array} modules Module names
173
- * @return {Promise} Resolves When all modules have loaded (or failed to load)
177
+ * @param {Array<string>} modules Module names to load
178
+ * @param {Object} [options] Loading options
179
+ * @param {boolean} [options.force=false] If true, logs errors and continues loading other modules when a module fails. If false, throws a DependencyError on first failure.
180
+ * @return {Promise<void>} Resolves when all modules have loaded (or failed to load in force mode)
181
+ * @throws {DependencyError} When a module fails to load and options.force is not true
174
182
  */
175
- async loadModules (modules) {
176
- await Promise.allSettled(modules.map(async m => {
183
+ async loadModules (modules, options = {}) {
184
+ await Promise.all(modules.map(async m => {
177
185
  try {
178
186
  await this.loadModule(m)
179
187
  } catch (e) {
188
+ if (options.force !== true) {
189
+ const error = new Error(`Failed to load '${m}'`)
190
+ error.name = 'DependencyError'
191
+ error.cause = e
192
+ throw error
193
+ }
180
194
  this.logError(`Failed to load '${m}',`, e)
181
195
  const deps = this.peerDependencies[m]
182
196
  if (deps && deps.length) {
@@ -189,9 +203,10 @@ class DependencyLoader {
189
203
  }
190
204
 
191
205
  /**
192
- * Waits for a single module to load
193
- * @param {String} modName Name of module to wait for
194
- * @return {Promise} Resolves with module instance on module.onReady
206
+ * Waits for a single module to load. Returns the instance (if loaded), or hooks into moduleLoadedHook to wait for it.
207
+ * @param {string} modName Name of module to wait for (accepts short names without 'adapt-authoring-' prefix)
208
+ * @return {Promise<Object>} Resolves with module instance when module.onReady completes
209
+ * @throws {Error} When module is missing from configs or has failed to load
195
210
  */
196
211
  async waitForModule (modName) {
197
212
  if (!this._configsLoaded) {
@@ -202,8 +217,9 @@ class DependencyLoader {
202
217
  if (!this.configs[modName]) {
203
218
  throw new Error(`Missing required module '${modName}'`)
204
219
  }
220
+ const DependencyError = new Error(`Dependency '${modName}' failed to load`)
205
221
  if (this.failedModules.includes(modName)) {
206
- throw new Error(`dependency '${modName}' failed to load`)
222
+ throw DependencyError
207
223
  }
208
224
  const instance = this.instances[modName]
209
225
  if (instance) {
@@ -211,23 +227,75 @@ class DependencyLoader {
211
227
  }
212
228
  return new Promise((resolve, reject) => {
213
229
  this.moduleLoadedHook.tap((error, instance) => {
214
- if (error) return reject(error)
230
+ if (error) return reject(DependencyError)
215
231
  if (instance?.name === modName) resolve(instance)
216
232
  })
217
233
  })
218
234
  }
219
235
 
220
236
  /**
221
- * Logs an error message
222
- * @param {...*} args Arguments to be printed
237
+ * Logs load progress
238
+ * @param {AbstractModule} instance The last loaded instance
223
239
  */
224
- logError (...args) {
225
- if (this.app.logger && this.app.logger._isReady) {
226
- this.app.logger.log('error', this.name, ...args)
240
+ logProgress (error, instance) {
241
+ if (error) {
242
+ return
243
+ }
244
+ const toShort = names => names.map(n => n.replace('adapt-authoring-', '')).join(', ')
245
+ const loaded = []
246
+ const notLoaded = []
247
+ let totalCount = 0
248
+ Object.keys(this.configs).forEach(key => {
249
+ if (this.configs[key].module === false) return
250
+ this.instances[key]?._isReady || key === instance.name ? loaded.push(key) : notLoaded.push(key)
251
+ totalCount++
252
+ })
253
+ const progress = Math.round((loaded.length / totalCount) * 100)
254
+ this.log('verbose', 'LOAD', [
255
+ toShort([instance.name]),
256
+ `${loaded.length}/${totalCount} (${progress}%)`,
257
+ notLoaded.length && `awaiting: ${toShort(notLoaded)}`,
258
+ this.failedModules.length && `failed: ${toShort(this.failedModules)}`
259
+ ].filter(Boolean).join(', '))
260
+
261
+ if (progress === 100) {
262
+ const initTimes = Object.entries(this.instances)
263
+ .sort((a, b) => a[1].initTime < b[1].initTime ? -1 : a[1].initTime > b[1].initTime ? 1 : 0)
264
+ .reduce((memo, [modName, instance]) => Object.assign(memo, { [modName]: instance.initTime }), {})
265
+ this.log('verbose', initTimes)
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Logs a message using the app logger if available, otherwise falls back to console.log
271
+ * @param {...*} args Arguments to be logged
272
+ */
273
+ log (level, ...args) {
274
+ if (this.app.logger?._isReady) {
275
+ this.app.logger.log(level, this.name, ...args)
227
276
  } else {
228
277
  console.log(...args)
229
278
  }
230
279
  }
280
+
281
+ /**
282
+ * Logs an error message using the app logger if available, otherwise falls back to console.log
283
+ * @param {...*} args Arguments to be logged
284
+ */
285
+ logError (...args) {
286
+ this.log('error', ...args)
287
+ }
288
+
289
+ /**
290
+ * Retrieves a configuration value from this module's config
291
+ * @param {string} key - The configuration key to retrieve
292
+ * @returns {*|undefined} The configuration value if config is ready, undefined otherwise
293
+ */
294
+ getConfig (key) {
295
+ if (this.app.config?._isReady) {
296
+ return this.app.config.get(`adapt-authoring-core.${key}`)
297
+ }
298
+ }
231
299
  }
232
300
 
233
301
  export default DependencyLoader
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-core",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "A bundle of reusable 'core' functionality",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-core",
6
6
  "license": "GPL-3.0",
@@ -1,11 +0,0 @@
1
- # To get started with Dependabot version updates, you'll need to specify which
2
- # package ecosystems to update and where the package manifests are located.
3
- # Please see the documentation for all configuration options:
4
- # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
-
6
- version: 2
7
- updates:
8
- - package-ecosystem: "npm" # See documentation for possible values
9
- directory: "/" # Location of package manifests
10
- schedule:
11
- interval: "weekly"