@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 +21 -0
- package/index.js +93 -54
- package/index.test.js +4 -1
- package/package.json +5 -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,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
|
|
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`
|
|
104
|
-
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} ⇢
|
|
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
|
-
|
|
122
|
+
Core.log.fail`${error}`
|
|
139
123
|
setImmediate(() => process.exit(1))
|
|
140
124
|
}
|
|
141
125
|
else if(reason)
|
|
142
126
|
{
|
|
143
|
-
|
|
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.#
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
542
|
+
const error = new Error(`${label} has reached the restart limit`)
|
|
519
543
|
error.code = 'E_CORE_CLUSTER_RESTART_LIMIT'
|
|
520
|
-
error.cause =
|
|
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(
|
|
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`
|
|
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`
|
|
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`
|
|
580
|
+
Core.log.info`assigned config ${configPath + '-' + this.branch}`
|
|
557
581
|
}
|
|
558
582
|
catch(error)
|
|
559
583
|
{
|
|
560
|
-
if(error.code
|
|
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 #
|
|
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('
|
|
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
|
|
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
|
|
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: {
|
|
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.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
|
|
13
|
-
"@superhero/config": "^4.
|
|
14
|
-
"@superhero/locator": "^4.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"
|