@superhero/core 4.0.0-beta.4 → 4.0.0-beta.6

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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Erik Landvall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import os from 'node:os'
2
2
  import cluster from 'node:cluster'
3
3
  import EventEmitter from 'node:events'
4
+ import path from 'node:path'
4
5
  import bootstrap from '@superhero/bootstrap'
5
6
  import Config from '@superhero/config'
6
- import { Locate } from '@superhero/locator'
7
+ import Locate from '@superhero/locator'
7
8
 
8
9
  export default class Core
9
10
  {
@@ -21,27 +22,6 @@ export default class Core
21
22
  Core.#setupDestructor(this)
22
23
  }
23
24
 
24
- static log(template, ...args)
25
- {
26
- const
27
- dim = '\x1b[1m\x1b[90m\x1b[2m',
28
- color = '\x1b[90m',
29
- mark = '\x1b[2m\x1b[96m',
30
- reset = '\x1b[0m',
31
- label = process.env.CORE_CLUSTER_WORKER
32
- ? `[CORE:${process.env.CORE_CLUSTER_WORKER}]`
33
- : '[CORE]',
34
- icon = ' ⇢ ',
35
- write = console._stdout.write.bind(console._stdout),
36
- build = template.reduce((result, part, i) =>
37
- {
38
- const arg = args[i - 1] ?? ''
39
- return result + mark + arg + reset + color + part
40
- })
41
-
42
- return write(reset + dim + label + icon + reset + color + build + reset + '\n')
43
- }
44
-
45
25
  #branch
46
26
  #workers = {}
47
27
  #basePath = false
@@ -100,14 +80,14 @@ export default class Core
100
80
  {
101
81
  if(Core.#destructorIsTriggered)
102
82
  {
103
- Core.log`Redundant shutdown signal, waiting for the shutdown process to finalize…`
104
- reason && console.error(reason)
83
+ Core.log.info`redundant shutdown signal ${signal} waiting for previous shutdown process to finalize…`
84
+ reason && Core.log.error(reason)
105
85
  return
106
86
  }
107
87
 
108
88
  Core.#destructorIsTriggered = true
109
89
 
110
- signal && Core.log`${signal} ⇢ Graceful shutdown initiated…`
90
+ signal && Core.log.info`${signal} ⇢ graceful shutdown initiated…`
111
91
 
112
92
  const
113
93
  destructCores = [],
@@ -135,12 +115,12 @@ export default class Core
135
115
  const error = new Error('Failed to shutdown gracefully')
136
116
  error.code = 'E_CORE_DESTRUCT_GRACEFUL'
137
117
  error.cause = destructRejects
138
- console.error(error)
118
+ Core.log.error(error)
139
119
  setImmediate(() => process.exit(1))
140
120
  }
141
121
  else if(reason)
142
122
  {
143
- console.error(reason)
123
+ Core.log.error(reason)
144
124
  setImmediate(() => process.exit(1))
145
125
  }
146
126
  else
@@ -292,15 +272,16 @@ export default class Core
292
272
  }
293
273
  else if(false === this.#isBooted)
294
274
  {
295
- await this.#addDependencies()
275
+ await this.#confirmAndAddDependencies()
276
+ await this.#confirmAbsoluteServcieMapPaths()
296
277
 
297
278
  freeze && this.config.freeze()
298
-
279
+
299
280
  const locatorMap = this.config.find('locator')
300
281
  locatorMap && await this.locate.eagerload(locatorMap)
301
282
 
302
283
  const bootstrapMap = this.config.find('bootstrap')
303
- bootstrapMap && await bootstrap.bootstrap(bootstrapMap, this.config, this.locate)
284
+ bootstrapMap && await bootstrap(bootstrapMap, this.config, this.locate)
304
285
 
305
286
  this.#isBooted = true
306
287
  }
@@ -348,7 +329,7 @@ export default class Core
348
329
  this.#workers[forkId].once('exit', this.#reloadWorker.bind(this, forkId, forkBranch, forkVersion))
349
330
  this.#workers[forkId].sync = this.#createSynchoronizer(forkId)
350
331
 
351
- Core.log`Cluster ${'CORE:' + forkId}`
332
+ Core.log.info`clustered ${'CORE:' + forkId}`
352
333
 
353
334
  try
354
335
  {
@@ -366,6 +347,44 @@ export default class Core
366
347
  return branch + forks
367
348
  }
368
349
 
350
+ /**
351
+ * Eagerload services by the configured locator service map.
352
+ *
353
+ * Resolves the absolute path if any of the services to eagerload is refferencing
354
+ * a relative path.
355
+ *
356
+ * Resolves the absolute path by the config entry, through the config instance.
357
+ */
358
+ async #confirmAbsoluteServcieMapPaths()
359
+ {
360
+ const locatorMap = this.config.find('locator')
361
+
362
+ if(locatorMap)
363
+ {
364
+ const serviceMap = this.locate.normaliseServiceMap(locatorMap)
365
+
366
+ for(const entry in serviceMap)
367
+ {
368
+ const servicePath = serviceMap[entry]
369
+
370
+ if('string' === typeof servicePath)
371
+ {
372
+ if(servicePath.startsWith('.'))
373
+ {
374
+ const
375
+ configPath = 'locator/' + entry,
376
+ absolutePath = this.config.findAbsoluteDirPathByConfigEntry(configPath, servicePath)
377
+
378
+ if('string' === typeof absolutePath)
379
+ {
380
+ serviceMap[entry] = path.normalize(absolutePath + '/' + servicePath)
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+
369
388
  #createSynchoronizer(id)
370
389
  {
371
390
  const
@@ -497,7 +516,8 @@ export default class Core
497
516
  {
498
517
  const
499
518
  failedToSynchronize = this.#workers[id].synchronizing,
500
- signal_code = signal ? `${signal}:${code}` : code
519
+ signal_code = signal ? `${signal}:${code}` : code,
520
+ label = `CORE:${id}`
501
521
 
502
522
  this.#workers[id].removeAllListeners()
503
523
  this.#workers[id].kill()
@@ -505,24 +525,24 @@ export default class Core
505
525
 
506
526
  if(0 === code)
507
527
  {
508
- Core.log`Worker ${id} finalized!`
528
+ Core.log.info`${label} ⇣ terminated`
509
529
  }
510
530
  else if('SIGTERM' === signal
511
531
  || 'SIGINT' === signal
512
532
  || 'SIGQUIT' === signal)
513
533
  {
514
- Core.log`Worker ${id} terminated [${signal_code}]`
534
+ Core.log.info`${label} terminated ${signal_code}`
515
535
  }
516
536
  else if(version >= this.config.find('core/cluster/restart/limit', 99))
517
537
  {
518
- const error = new Error(`Worker ${id} has reached the restart limit`)
538
+ const error = new Error(`${label} has reached the restart limit`)
519
539
  error.code = 'E_CORE_CLUSTER_RESTART_LIMIT'
520
- error.cause = `Worker ${id} terminated after unexpected interruption [${signal_code}]`
540
+ error.cause = `${label} process crashed ${signal_code}`
521
541
  throw error
522
542
  }
523
543
  else if(failedToSynchronize)
524
544
  {
525
- const error = new Error(`Worker ${id} failed to synchronize`)
545
+ const error = new Error(`${label} failed to synchronize`)
526
546
  error.code = 'E_CORE_CLUSTER_SYNC_FAILED'
527
547
  throw error
528
548
  }
@@ -530,7 +550,7 @@ export default class Core
530
550
  {
531
551
  try
532
552
  {
533
- Core.log`Restart worker ${id} after unexpected interruption [${signal_code}]`
553
+ Core.log.info`restarting ${label} previous process crashed ⇠ ${signal_code}`
534
554
  await this.cluster(1, branch, version + 1)
535
555
  }
536
556
  catch(reason)
@@ -546,22 +566,18 @@ export default class Core
546
566
  async #addConfigPath(configPath)
547
567
  {
548
568
  await this.config.add(configPath)
549
- Core.log`Assigned config ${configPath}`
569
+ Core.log.info`assigned config ${configPath}`
550
570
 
551
571
  if(this.branch)
552
572
  {
553
573
  try
554
574
  {
555
575
  await this.config.add(configPath, this.branch)
556
- Core.log`Assigned config ${configPath + '-' + this.branch}`
576
+ Core.log.info`assigned config ${configPath + '-' + this.branch}`
557
577
  }
558
578
  catch(error)
559
579
  {
560
- if(error.code === 'E_CONFIG_ADD')
561
- {
562
- Core.log`Failed to assign config ${configPath}-${this.branch} ⇢ ${error.message}`
563
- }
564
- else
580
+ if(error.code !== 'E_CONFIG_ADD')
565
581
  {
566
582
  throw error
567
583
  }
@@ -569,7 +585,7 @@ export default class Core
569
585
  }
570
586
  }
571
587
 
572
- async #addDependencies()
588
+ async #confirmAndAddDependencies()
573
589
  {
574
590
  const loaded = []
575
591
 
@@ -589,21 +605,40 @@ export default class Core
589
605
 
590
606
  #findNotLodadedDependencies(loaded)
591
607
  {
592
- const dependencies = this.config.find('dependency')
593
-
594
- if(undefined === dependencies)
595
- {
596
- return []
597
- }
608
+ const dependencies = this.config.find('core/dependencies',
609
+ this.config.find('core/dependency', []))
598
610
 
599
611
  if(false === Array.isArray(dependencies))
600
612
  {
601
- const error = new TypeError(`Invalid dependency type: ${Object.prototype.toString.call(dependencies)}`)
613
+ const error = new TypeError(`Invalid dependencies type: ${Object.prototype.toString.call(dependencies)}`)
602
614
  error.code = 'E_CORE_CONFIG_DEPENDENCY_INVALID_TYPE'
603
- error.cause = 'Config dependency must be an array'
615
+ error.cause = 'Config core/dependencies must be an array'
604
616
  throw error
605
617
  }
606
618
 
619
+ for(let i = 0; i < dependencies.length; i++)
620
+ {
621
+ const dependency = dependencies[i]
622
+
623
+ if('string' === typeof dependency)
624
+ {
625
+ const error = new TypeError(`Invalid dependency type: ${Object.prototype.toString.call(dependency)}`)
626
+ error.code = 'E_CORE_CONFIG_DEPENDENCY_INVALID_TYPE'
627
+ error.cause = 'The dependency values in config core/dependencies must be of type string'
628
+ throw error
629
+ }
630
+
631
+ if(dependency.startsWith('.'))
632
+ {
633
+ const absolutePath = this.config.findAbsoluteDirPathByConfigEntry('core/dependencies', [ dependency ])
634
+
635
+ if('string' === typeof absolutePath)
636
+ {
637
+ dependencies[i] = path.normalize(absolutePath + '/' + dependency)
638
+ }
639
+ }
640
+ }
641
+
607
642
  return dependencies.filter((dependency) => false === loaded.includes(dependency))
608
643
  }
609
644
 
@@ -699,4 +734,22 @@ export default class Core
699
734
  throw error
700
735
  }
701
736
  }
737
+
738
+ // Will make the log methods availible to overwrite
739
+ // if a custom logging is desired.
740
+ static log =
741
+ {
742
+ label : process.env.CORE_CLUSTER_WORKER
743
+ ? `[CORE:${process.env.CORE_CLUSTER_WORKER}]`
744
+ : '[CORE]',
745
+ icon : ' ⇢ ',
746
+ basic : (template, ...args) => template.reduce((result, part, i) => result + args[i - 1] + part),
747
+ simple : (template, ...args) => this.log.label + this.log.icon + this.log.basic(template, ...args),
748
+ colors : (template, ...args) => '\x1b[90m' + this.log.supress`${this.log.label + this.log.icon}` + template.reduce((result, part, i) => result + '\x1b[96m\x1b[2m' + args[i - 1] + '\x1b[90m' + part) + '\x1b[0m',
749
+ supress : (template, ...args) => '\x1b[1m\x1b[2m' + this.log.basic(template, ...args) + '\x1b[22m',
750
+ format : (...args) => this.log.colors(...args),
751
+ info : (...args) => console.info (this.log.format(...args)),
752
+ warning : (...args) => console.warn (this.log.format(...args)),
753
+ error : (...args) => console.error (this.log.format`failure`, ...args)
754
+ }
702
755
  }
package/index.test.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import assert from 'node:assert'
2
2
  import fs from 'node:fs/promises'
3
3
  import Core from '@superhero/core'
4
+ import util from 'node:util'
4
5
  import { before, after, suite, test } from 'node:test'
5
6
 
7
+ util.inspect.defaultOptions.depth = 5
8
+
6
9
  suite('@superhero/core', () =>
7
10
  {
8
11
  const
@@ -21,7 +24,7 @@ suite('@superhero/core', () =>
21
24
  await fs.writeFile(fooService, 'export default class Foo { static locate() { return new Foo() } bootstrap(config) { this.bar = config.bar } }')
22
25
 
23
26
  // Create mock config files
24
- await fs.writeFile(fooConfig, JSON.stringify({ foo: { bar: 'baz' }, bootstrap: { foo: true }, locator: { 'foo': fooService } }))
27
+ await fs.writeFile(fooConfig, JSON.stringify({ foo: { bar: 'baz' }, bootstrap: { foo: true }, locator: { foo: './service.js' } }))
25
28
  await fs.writeFile(fooConfigDev, JSON.stringify({ foo: { bar: 'qux' } }))
26
29
 
27
30
  // Disable console output for the tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superhero/core",
3
- "version": "4.0.0-beta.4",
3
+ "version": "4.0.0-beta.6",
4
4
  "description": "Core functionalities for the superhero framework.",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -9,9 +9,9 @@
9
9
  ".": "./index.js"
10
10
  },
11
11
  "dependencies": {
12
- "@superhero/bootstrap": "^4.0.1",
13
- "@superhero/config": "^4.0.6",
14
- "@superhero/locator": "^4.0.4"
12
+ "@superhero/bootstrap": "^4.1.0",
13
+ "@superhero/config": "^4.1.2",
14
+ "@superhero/locator": "^4.1.2"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --trace-warnings --test --experimental-test-coverage"