@superhero/core 4.0.0-beta.5 → 4.0.0-beta.7

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,11 @@
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
7
  import Locate from '@superhero/locator'
8
+ import Log from '@superhero/log'
7
9
 
8
10
  export default class Core
9
11
  {
@@ -21,27 +23,6 @@ export default class Core
21
23
  Core.#setupDestructor(this)
22
24
  }
23
25
 
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
26
  #branch
46
27
  #workers = {}
47
28
  #basePath = false
@@ -51,7 +32,7 @@ export default class Core
51
32
  {
52
33
  get: (target, prop, receiver) =>
53
34
  {
54
- // when pushing events to the eventlog, also send the events to all workers
35
+ // when pushing events to the eventlog, also sync the event to all workers
55
36
  if('push' === prop) return (...events) =>
56
37
  {
57
38
  setImmediate(() =>
@@ -68,6 +49,9 @@ export default class Core
68
49
  }
69
50
  })
70
51
 
52
+ // An exposed log instance taht the core instance uses that is open for modification.
53
+ static log = new Log({ label: process.env.CORE_CLUSTER_WORKER ? `[CORE:${process.env.CORE_CLUSTER_WORKER}]` : '[CORE]' })
54
+
71
55
  // Used for graceful termination, if multiple cores are instanciated.
72
56
  static #cores = new Map
73
57
 
@@ -100,14 +84,14 @@ export default class Core
100
84
  {
101
85
  if(Core.#destructorIsTriggered)
102
86
  {
103
- Core.log`Redundant shutdown signal, waiting for the shutdown process to finalize…`
104
- reason && console.error(reason)
87
+ Core.log.info`redundant shutdown signal ${signal} waiting for previous shutdown process to finalize…`
88
+ reason && Core.log.fail`${reason}`
105
89
  return
106
90
  }
107
91
 
108
92
  Core.#destructorIsTriggered = true
109
93
 
110
- signal && Core.log`${signal} ⇢ Graceful shutdown initiated…`
94
+ signal && Core.log.info`${signal} ⇢ graceful shutdown initiated…`
111
95
 
112
96
  const
113
97
  destructCores = [],
@@ -135,12 +119,12 @@ export default class Core
135
119
  const error = new Error('Failed to shutdown gracefully')
136
120
  error.code = 'E_CORE_DESTRUCT_GRACEFUL'
137
121
  error.cause = destructRejects
138
- console.error(error)
122
+ Core.log.fail`${error}`
139
123
  setImmediate(() => process.exit(1))
140
124
  }
141
125
  else if(reason)
142
126
  {
143
- console.error(reason)
127
+ Core.log.fail`${reason}`
144
128
  setImmediate(() => process.exit(1))
145
129
  }
146
130
  else
@@ -292,15 +276,16 @@ export default class Core
292
276
  }
293
277
  else if(false === this.#isBooted)
294
278
  {
295
- await this.#addDependencies()
279
+ await this.#confirmAndAddDependencies()
280
+ await this.#confirmAbsoluteServcieMapPaths()
296
281
 
297
282
  freeze && this.config.freeze()
298
-
283
+
299
284
  const locatorMap = this.config.find('locator')
300
285
  locatorMap && await this.locate.eagerload(locatorMap)
301
286
 
302
287
  const bootstrapMap = this.config.find('bootstrap')
303
- bootstrapMap && await bootstrap.bootstrap(bootstrapMap, this.config, this.locate)
288
+ bootstrapMap && await bootstrap(bootstrapMap, this.config, this.locate)
304
289
 
305
290
  this.#isBooted = true
306
291
  }
@@ -348,7 +333,7 @@ export default class Core
348
333
  this.#workers[forkId].once('exit', this.#reloadWorker.bind(this, forkId, forkBranch, forkVersion))
349
334
  this.#workers[forkId].sync = this.#createSynchoronizer(forkId)
350
335
 
351
- Core.log`Cluster ${'CORE:' + forkId}`
336
+ Core.log.info`clustered ${'CORE:' + forkId}`
352
337
 
353
338
  try
354
339
  {
@@ -366,6 +351,44 @@ export default class Core
366
351
  return branch + forks
367
352
  }
368
353
 
354
+ /**
355
+ * Eagerload services by the configured locator service map.
356
+ *
357
+ * Resolves the absolute path if any of the services to eagerload is refferencing
358
+ * a relative path.
359
+ *
360
+ * Resolves the absolute path by the config entry, through the config instance.
361
+ */
362
+ async #confirmAbsoluteServcieMapPaths()
363
+ {
364
+ const locatorMap = this.config.find('locator')
365
+
366
+ if(locatorMap)
367
+ {
368
+ const serviceMap = this.locate.normaliseServiceMap(locatorMap)
369
+
370
+ for(const entry in serviceMap)
371
+ {
372
+ const servicePath = serviceMap[entry]
373
+
374
+ if('string' === typeof servicePath)
375
+ {
376
+ if(servicePath.startsWith('.'))
377
+ {
378
+ const
379
+ configPath = 'locator/' + entry,
380
+ absolutePath = this.config.findAbsoluteDirPathByConfigEntry(configPath, servicePath)
381
+
382
+ if('string' === typeof absolutePath)
383
+ {
384
+ serviceMap[entry] = path.normalize(path.join(absolutePath, servicePath))
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+
369
392
  #createSynchoronizer(id)
370
393
  {
371
394
  const
@@ -497,7 +520,8 @@ export default class Core
497
520
  {
498
521
  const
499
522
  failedToSynchronize = this.#workers[id].synchronizing,
500
- signal_code = signal ? `${signal}:${code}` : code
523
+ signal_code = signal ? `${signal}:${code}` : code,
524
+ label = `CORE:${id}`
501
525
 
502
526
  this.#workers[id].removeAllListeners()
503
527
  this.#workers[id].kill()
@@ -505,24 +529,24 @@ export default class Core
505
529
 
506
530
  if(0 === code)
507
531
  {
508
- Core.log`Worker ${id} finalized!`
532
+ Core.log.info`${label} ⇣ terminated`
509
533
  }
510
534
  else if('SIGTERM' === signal
511
535
  || 'SIGINT' === signal
512
536
  || 'SIGQUIT' === signal)
513
537
  {
514
- Core.log`Worker ${id} terminated [${signal_code}]`
538
+ Core.log.info`${label} terminated ${signal_code}`
515
539
  }
516
540
  else if(version >= this.config.find('core/cluster/restart/limit', 99))
517
541
  {
518
- const error = new Error(`Worker ${id} has reached the restart limit`)
542
+ const error = new Error(`${label} has reached the restart limit`)
519
543
  error.code = 'E_CORE_CLUSTER_RESTART_LIMIT'
520
- error.cause = `Worker ${id} terminated after unexpected interruption [${signal_code}]`
544
+ error.cause = `${label} process crashed ${signal_code}`
521
545
  throw error
522
546
  }
523
547
  else if(failedToSynchronize)
524
548
  {
525
- const error = new Error(`Worker ${id} failed to synchronize`)
549
+ const error = new Error(`${label} failed to synchronize`)
526
550
  error.code = 'E_CORE_CLUSTER_SYNC_FAILED'
527
551
  throw error
528
552
  }
@@ -530,7 +554,7 @@ export default class Core
530
554
  {
531
555
  try
532
556
  {
533
- Core.log`Restart worker ${id} after unexpected interruption [${signal_code}]`
557
+ Core.log.info`restarting ${label} previous process crashed ⇠ ${signal_code}`
534
558
  await this.cluster(1, branch, version + 1)
535
559
  }
536
560
  catch(reason)
@@ -546,22 +570,18 @@ export default class Core
546
570
  async #addConfigPath(configPath)
547
571
  {
548
572
  await this.config.add(configPath)
549
- Core.log`Assigned config ${configPath}`
573
+ Core.log.info`assigned config ${configPath}`
550
574
 
551
575
  if(this.branch)
552
576
  {
553
577
  try
554
578
  {
555
579
  await this.config.add(configPath, this.branch)
556
- Core.log`Assigned config ${configPath + '-' + this.branch}`
580
+ Core.log.info`assigned config ${configPath + '-' + this.branch}`
557
581
  }
558
582
  catch(error)
559
583
  {
560
- if(error.code === 'E_CONFIG_ADD')
561
- {
562
- Core.log`Failed to assign config ${configPath}-${this.branch} ⇢ ${error.message}`
563
- }
564
- else
584
+ if(error.code !== 'E_CONFIG_ADD')
565
585
  {
566
586
  throw error
567
587
  }
@@ -569,7 +589,7 @@ export default class Core
569
589
  }
570
590
  }
571
591
 
572
- async #addDependencies()
592
+ async #confirmAndAddDependencies()
573
593
  {
574
594
  const loaded = []
575
595
 
@@ -589,21 +609,40 @@ export default class Core
589
609
 
590
610
  #findNotLodadedDependencies(loaded)
591
611
  {
592
- const dependencies = this.config.find('dependency')
593
-
594
- if(undefined === dependencies)
595
- {
596
- return []
597
- }
612
+ const dependencies = this.config.find('core/dependencies',
613
+ this.config.find('core/dependency', []))
598
614
 
599
615
  if(false === Array.isArray(dependencies))
600
616
  {
601
- const error = new TypeError(`Invalid dependency type: ${Object.prototype.toString.call(dependencies)}`)
617
+ const error = new TypeError(`Invalid dependencies type: ${Object.prototype.toString.call(dependencies)}`)
602
618
  error.code = 'E_CORE_CONFIG_DEPENDENCY_INVALID_TYPE'
603
- error.cause = 'Config dependency must be an array'
619
+ error.cause = 'Config core/dependencies must be an array'
604
620
  throw error
605
621
  }
606
622
 
623
+ for(let i = 0; i < dependencies.length; i++)
624
+ {
625
+ const dependency = dependencies[i]
626
+
627
+ if('string' === typeof dependency)
628
+ {
629
+ const error = new TypeError(`Invalid dependency type: ${Object.prototype.toString.call(dependency)}`)
630
+ error.code = 'E_CORE_CONFIG_DEPENDENCY_INVALID_TYPE'
631
+ error.cause = 'The dependency values in config core/dependencies must be of type string'
632
+ throw error
633
+ }
634
+
635
+ if(dependency.startsWith('.'))
636
+ {
637
+ const absolutePath = this.config.findAbsoluteDirPathByConfigEntry('core/dependencies', [ dependency ])
638
+
639
+ if('string' === typeof absolutePath)
640
+ {
641
+ dependencies[i] = path.normalize(path.join(absolutePath, dependency))
642
+ }
643
+ }
644
+ }
645
+
607
646
  return dependencies.filter((dependency) => false === loaded.includes(dependency))
608
647
  }
609
648
 
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.5",
3
+ "version": "4.0.0-beta.7",
4
4
  "description": "Core functionalities for the superhero framework.",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -9,9 +9,10 @@
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.1.1"
12
+ "@superhero/bootstrap": "^4.1.0",
13
+ "@superhero/config": "^4.1.2",
14
+ "@superhero/locator": "^4.1.2",
15
+ "@superhero/log": "^4.0.0"
15
16
  },
16
17
  "scripts": {
17
18
  "test": "node --trace-warnings --test --experimental-test-coverage"