@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 +21 -0
- package/index.js +107 -54
- package/index.test.js +4 -1
- package/package.json +4 -4
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
|
|
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`
|
|
104
|
-
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} ⇢
|
|
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
|
-
|
|
118
|
+
Core.log.error(error)
|
|
139
119
|
setImmediate(() => process.exit(1))
|
|
140
120
|
}
|
|
141
121
|
else if(reason)
|
|
142
122
|
{
|
|
143
|
-
|
|
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.#
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
538
|
+
const error = new Error(`${label} has reached the restart limit`)
|
|
519
539
|
error.code = 'E_CORE_CLUSTER_RESTART_LIMIT'
|
|
520
|
-
error.cause =
|
|
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(
|
|
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`
|
|
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`
|
|
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`
|
|
576
|
+
Core.log.info`assigned config ${configPath + '-' + this.branch}`
|
|
557
577
|
}
|
|
558
578
|
catch(error)
|
|
559
579
|
{
|
|
560
|
-
if(error.code
|
|
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 #
|
|
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('
|
|
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
|
|
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
|
|
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: {
|
|
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.
|
|
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
|
|
13
|
-
"@superhero/config": "^4.
|
|
14
|
-
"@superhero/locator": "^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"
|