├── .all-contributorsrc
├── .codeclimate.yml
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── code-climate-test-coverage.yml
│ ├── node-js-ci.yml
│ └── node-js-publish.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── .versionrc
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── _config.yml
├── assets
├── banner-transparent.png
├── banner-transparent.svg
├── banner.png
├── banner.svg
├── icon.png
└── icon.svg
├── docs
├── .nojekyll
└── index.html
├── examples
├── bootstrapped
│ ├── components
│ │ ├── config
│ │ │ ├── config.js
│ │ │ └── index.js
│ │ ├── logger
│ │ │ ├── index.js
│ │ │ └── logger.js
│ │ └── mongo
│ │ │ ├── index.js
│ │ │ └── mongo.js
│ ├── index.js
│ └── system.js
└── simple
│ ├── components
│ ├── config.js
│ ├── logger.js
│ └── mongo.js
│ ├── index.js
│ └── system.js
├── index.d.ts
├── index.js
├── package-lock.json
├── package.json
├── test
├── .eslintrc.json
├── components
│ ├── bar
│ │ └── index.js
│ └── foo
│ │ └── index.js
└── systemic.test.js
└── utils.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "commitType": "docs",
8 | "commitConvention": "angular",
9 | "contributors": [
10 | {
11 | "login": "inigomarquinez",
12 | "name": "Íñigo Marquínez Prado",
13 | "avatar_url": "https://avatars.githubusercontent.com/u/25435858?v=4",
14 | "profile": "https://github.com/inigomarquinez",
15 | "contributions": [
16 | "doc",
17 | "maintenance",
18 | "review"
19 | ]
20 | }
21 | ],
22 | "contributorsPerLine": 7,
23 | "skipCi": true,
24 | "repoType": "github",
25 | "repoHost": "https://github.com",
26 | "projectName": "systemic",
27 | "projectOwner": "onebeyond"
28 | }
29 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | engines:
3 | eslint:
4 | enabled: false
5 | duplication:
6 | enabled: true
7 | config:
8 | languages:
9 | - javascript
10 | checks:
11 | method-complexity:
12 | enabled: false
13 | method-lines:
14 | enabled: false
15 | ratings:
16 | paths:
17 | - index.js
18 | exclude_patterns:
19 | - 'examples/'
20 | - 'test/'
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | examples
2 | coverage
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["eslint-config-airbnb-base", "prettier"],
3 | "env": {
4 | "node": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": "2017"
8 | },
9 | "rules": {
10 | "consistent-return": 0,
11 | "func-names": 0,
12 | "prefer-object-spread": 0,
13 | "no-param-reassign": 0,
14 | "no-plusplus": 0,
15 | "no-prototype-builtins": 0,
16 | "no-shadow": ["error", { "allow": ["err", "cb"] }],
17 | "no-underscore-dangle": 0,
18 | "no-unused-expressions": 0,
19 | "no-use-before-define": 0
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/code-climate-test-coverage.yml:
--------------------------------------------------------------------------------
1 | name: Code Climate Test Reporter
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | permissions: # added using https://github.com/step-security/secure-workflows
10 | contents: read
11 |
12 | jobs:
13 | code-climate:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Harden Runner
17 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1
18 | with:
19 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
20 |
21 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
22 | - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
23 | with:
24 | node-version: '18.x'
25 | - run: npm ci
26 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
27 | - run: chmod +x ./cc-test-reporter
28 | - run: ./cc-test-reporter before-build
29 | - run: npm run coverage
30 | - run: ./cc-test-reporter format-coverage -t lcov coverage/lcov.info
31 | - run: ./cc-test-reporter upload-coverage
32 | env:
33 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/node-js-ci.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | node-version: [12.x, 14.x, 16.x]
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v2
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - run: npm ci
17 | - run: npm run lint
18 | - run: npm run prettier
19 | - run: npm test
20 |
21 | code-climate:
22 | needs: build
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v2
26 | - uses: actions/setup-node@v2
27 | with:
28 | node-version: '16.x'
29 | - run: npm ci
30 | - run: npm install -g nyc
31 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
32 | - run: chmod +x ./cc-test-reporter
33 | - run: ./cc-test-reporter before-build
34 | - run: npm run coverage
35 | - run: ./cc-test-reporter format-coverage -t lcov coverage/lcov.info
36 | - run: ./cc-test-reporter upload-coverage
37 | env:
38 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/node-js-publish.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Publish
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v2
13 | with:
14 | node-version: '16.x'
15 | - run: npm ci
16 | - run: npm run lint
17 | - run: npm run prettier
18 | - run: npm test
19 |
20 | publish-npm:
21 | needs: build
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v2
25 | - uses: actions/setup-node@v2
26 | with:
27 | node-version: '16.x'
28 | registry-url: https://registry.npmjs.org/
29 | - run: npm ci
30 | - run: npm publish
31 | env:
32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | *.log
4 | .codeclimate
5 | .nyc_output/
6 |
7 |
8 | ## IDE
9 | .idea
10 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged && npm test
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .codeclimate.yml
3 | .idea
4 | .eslintignore
5 | .eslintrc
6 | .github
7 | .nyc_output
8 | _config.yml
9 | coverage
10 | cc-test-reporter
11 | docs
12 | node_modules
13 | test
14 | assets
15 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | coverage
2 | .nyc_output
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 120
4 | }
5 |
--------------------------------------------------------------------------------
/.versionrc:
--------------------------------------------------------------------------------
1 | {
2 | "types": [
3 | { "type": "feat", "section": "🆕 Features", "hidden": false },
4 | { "type": "fix", "section": "🐛 Bug Fixes", "hidden": false },
5 | { "type": "chore", "section": "🔧 Others", "hidden": false },
6 | { "type": "docs", "section": "📝 Docs", "hidden": false },
7 | { "type": "style", "section": "🎨 Styling", "hidden": false },
8 | { "type": "refactor", "section": "🔄 Code Refactoring", "hidden": false },
9 | { "type": "perf", "section": "📈 Performance Improvements", "hidden": false },
10 | { "type": "test", "section": "🔬 Tests", "hidden": false },
11 | { "type": "ci", "section": "☁️ CI", "hidden": false }
12 | ],
13 | "commit-all": true
14 | }
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 4.1.2
4 |
5 | - Fix typescript definitions via . Thanks [PR60](https://github.com/guidesmiths/systemic/pull/60). Thanks [teunmooij](https://github.com/teunmooij).
6 | - Remove unused dev deps
7 |
8 | ## 4.1.1
9 |
10 | - Use eslint-config-airbnb-base
11 | - Move debug to prod dependency
12 |
13 | ## 4.1.0
14 |
15 | - Added optional dependencies
16 | - Updated typescript definitions to better reflect all options of Systemic:
17 | - optional type param on `Systemic` to set assumed components of master system when creating a subsystem
18 | - allow setting a simple dependency on component that doesn't need it (to force the order in which dependencies are created)
19 |
20 | ## 4.0.3
21 |
22 | - Remove standard-version because it is deprecated and doesnt add much value
23 | - Stop prettier wrapping markdown examples
24 |
25 | ## 4.0.2
26 |
27 | - added assets (logo and banner) ([dc4d534](https://github.com/guidesmiths/systemic/commit/dc4d534edefbec92bff085ea16e5cdd99a8e8956))
28 | - improved readme adding new assets ([6d1309d](https://github.com/guidesmiths/systemic/commit/6d1309de4e02c01a5bfdf35fe15879e4080ad1ab))
29 | - added assets folder to npmignore ([cb35c07](https://github.com/guidesmiths/systemic/commit/cb35c0756780fdae82766732653f51c9729ee44b))
30 |
31 | ## 4.0.1
32 |
33 | - Remove chai
34 | - Remove chance
35 | - Update dependencies
36 | - Tweak GitHub actions
37 | - Improve readme
38 |
39 | ## 4.0.0
40 |
41 | - Introduce prettier
42 | - Updated dependendencies
43 | - Drop support for Node 10
44 |
45 | ## 3.3.10
46 |
47 | - Added typescript definitions
48 |
49 | ## 3.3.9
50 |
51 | - Exclude various files (including the 12M cc-test-reporter binary) from the npm package.
52 |
53 | ## 3.3.8
54 |
55 | - Remove lodash
56 | - Replace mocha with zunit
57 | - Update nyc
58 | - Replace travis with github actions
59 | - Replace eslint imperative with ESNext and updated syntax
60 | - Bump dependencies
61 |
62 | ## 3.3.7
63 |
64 | - Tweak deployment
65 |
66 | ## 3.3.6
67 |
68 | - Tweak deployment
69 |
70 | ## 3.3.5
71 |
72 | - Tweak deployment
73 |
74 | ## 3.3.4
75 |
76 | - Tweak deployment
77 |
78 | ## 3.3.3
79 |
80 | - Automate codeclimate
81 | - Tweak deployment
82 |
83 | ## 3.3.1
84 |
85 | - Housekeeping
86 |
87 | ## 3.3.0
88 |
89 | - Updated dependencies
90 |
91 | ## 3.2.0
92 |
93 | - Updated dependencies
94 |
95 | ## 3.1.0
96 |
97 | - Updated dependencies
98 | - Dropped node 4 and 5 support
99 |
100 | ## 3.0.0
101 |
102 | ### Breaking Changes
103 |
104 | - Component start and stop functions can return promises instead of taking callbacks.
105 | To support this change callback based compnents start functions are no longer varardic, and must always specify both the dependencies and callback arguments, i.e. `function start(dependencies, cb) { ... }`.
106 |
107 | - Bootstrapped systems support export default syntax
108 |
109 | ## 2.2.0
110 |
111 | - Bootstrapping supports sub systems wrapped in functions
112 | - Improve README
113 | - Improve examples
114 |
115 | ## 2.1.0
116 |
117 | - Fix bug where if you destructured components in the system start callback and a component errored you received a "Cannot destructure property" error
118 | - Bootstrapping supports sub systems wrapped in functions
119 | - Improve README
120 |
121 | ## 2.0.0
122 |
123 | - System lifecycle methods (start, stop, restart) return promises
124 |
125 | ### Updated
126 |
127 | - All dependencies to latest compatible versions
128 |
129 | ### Removed
130 |
131 | - codeclimate-test-report (install globally if needed)
132 |
133 | ### Updated
134 |
135 | - Readme
136 |
137 | ## 1.3.3
138 |
139 | - Removed accidental console.log
140 | - Updated dependencies
141 |
142 | ## 1.3.2
143 |
144 | - Updated dev dependencies
145 |
146 | ## 1.3.1
147 |
148 | - Codeclimate automatically runs on push
149 |
150 | ## 1.3.0
151 |
152 | - Fixed coverage badges
153 | - Configuring code climate
154 |
155 | ## 1.2.3
156 |
157 | - Node 7 build
158 |
159 | ## 1.2.2
160 |
161 | - More badges
162 |
163 | ## 1.2.1
164 |
165 | - .npmignore
166 |
167 | ## 1.2.0
168 |
169 | - This changelog
170 | - License
171 | - Badges
172 |
173 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
174 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016-2022 GuideSmiths Ltd.
4 | Copyright (c) One Beyond 2022 to present.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 |
6 |
7 | 📦 A minimal dependency injection library.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## tl;dr
23 |
24 | ### Define the system
25 |
26 |
27 | ```js
28 | const Systemic = require('systemic');
29 | const Config = require('./components/config');
30 | const Logger = require('./components/logger');
31 | const Mongo = require('./components/mongo');
32 |
33 | module.exports = () => Systemic()
34 | .add('config', Config(), { scoped: true })
35 | .add('logger', Logger()).dependsOn('config')
36 | .add('mongo.primary', Mongo()).dependsOn('config', 'logger')
37 | .add('mongo.secondary', Mongo()).dependsOn('config', 'logger');
38 | ```
39 |
40 |
41 | ### Run the system
42 |
43 |
44 | ```js
45 | const System = require('./system');
46 |
47 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };
48 |
49 | async function start() {
50 | const system = System();
51 | const { config, mongo, logger } = await system.start();
52 |
53 | console.log('System has started. Press CTRL+C to stop');
54 |
55 | Object.keys(events).forEach((name) => {
56 | process.on(name, async () => {
57 | await system.stop();
58 | console.log('System has stopped');
59 | process.exit(events[name]);
60 | });
61 | });
62 | }
63 |
64 | start();
65 | ```
66 |
67 |
68 | See the [examples](https://github.com/onebeyond/systemic/tree/master/examples) for mode details and don't miss the section on [bootstrapping](#bootstraping-components) for how to organise large projects.
69 |
70 | ### Why Use Dependency Injection With Node.js?
71 |
72 | Node.js applications tend to be small and have few layers than applications developed in other languages such as Java. This reduces the benefit of dependency injection, which encouraged [the Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle), discouraged [God Objects](https://en.wikipedia.org/wiki/God_object) and facilitated unit testing through [test doubles](https://en.wikipedia.org/wiki/Test_double).
73 |
74 | We've found that when writing microservices the life cycle of an application and its dependencies is a nuisance to manage over and over again. We wanted a way to consistently express that our service should establish database connections before listening for http requests, and shutdown those connections only after it had stopped listening. We found that before doing anything we need to load config from remote sources, and configure loggers. This is why we use DI.
75 |
76 | Our first attempt at a dependency injection framework was [Electrician](https://www.npmjs.com/package/electrician). It served it's purpose well, but the API had a couple of limitations that we wanted to fix. This would have required a backwards incompatible change, so instead we decided to write a new DI library - Systemic.
77 |
78 | ### Concepts
79 |
80 | Systemic has 4 main concepts
81 |
82 | 1. Systems
83 | 1. Runners
84 | 1. Components
85 | 1. Dependencies
86 |
87 | #### Systems
88 |
89 | You add components and their dependencies to a system. When you start the system, systemic iterates through all the components, starting them in the order derived from the dependency graph. When you stop the system, systemic iterates through all the components stopping them in the reverse order.
90 |
91 |
92 | ```js
93 | const Systemic = require('systemic');
94 | const Config = require('./components/config');
95 | const Logger = require('./components/logger');
96 | const Mongo = require('./components/mongo');
97 |
98 | async function init() {
99 | const system = Systemic()
100 | .add('config', Config(), { scoped: true })
101 | .add('logger', Logger())
102 | .dependsOn('config')
103 | .add('mongo.primary', Mongo())
104 | .dependsOn('config', 'logger')
105 | .add('mongo.secondary', Mongo())
106 | .dependsOn('config', 'logger');
107 |
108 | const { config, mongo, logger } = await system.start();
109 |
110 | console.log('System has started. Press CTRL+C to stop');
111 |
112 | Object.keys(events).forEach((name) => {
113 | process.on(name, async () => {
114 | await system.stop();
115 | console.log('System has stopped');
116 | process.exit(events[name]);
117 | });
118 | });
119 | }
120 |
121 | init();
122 | ```
123 |
124 |
125 | System life cycle functions (start, stop, restart) return a promise, but can also take callbacks.
126 |
127 | ### Runners
128 |
129 | While not shown in the above examples we usually separate the system definition from system start. This is important for testing since you often want to make changes to the system definition (e.g. replacing components with stubs), before starting the system. By wrapping the system definition in a function you create a new system in each of your tests.
130 |
131 |
132 | ```js
133 | // system.js
134 | module.exports = () => Systemic().add('config', Config()).add('logger', Logger()).dependsOn('config').add('mongo', Mongo()).dependsOn('config', 'logger');
135 | ```
136 |
137 |
138 |
139 | ```js
140 | // index.js
141 | const System = require('./system');
142 |
143 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };
144 |
145 | async function start() {
146 | const system = System();
147 | const { config, mongo, logger } = await system.start();
148 |
149 | console.log('System has started. Press CTRL+C to stop');
150 |
151 | Object.keys(events).forEach((name) => {
152 | process.on(name, async () => {
153 | await system.stop();
154 | console.log('System has stopped');
155 | process.exit(events[name]);
156 | });
157 | });
158 | }
159 |
160 | start();
161 | ```
162 |
163 |
164 | There are some out of the box runners we can be used in your applications or as a reference for your own custom runner
165 |
166 | 1. [Service Runner](https://github.com/onebeyond/systemic-service-runner)
167 | 1. [Domain Runner](https://github.com/onebeyond/systemic-domain-runner)
168 |
169 | #### Components
170 |
171 | A component is an object with optional asynchronous start and stop functions. The start function should yield the underlying resource after it has been started. e.g.
172 |
173 |
174 | ```js
175 | module.exports = () => {
176 | let db;
177 |
178 | async function start(dependencies) {
179 | db = await MongoClient.connect('mongo://localhost/example');
180 | return db;
181 | }
182 |
183 | async function stop() {
184 | return db.close();
185 | }
186 |
187 | return {
188 | start: start,
189 | stop: stop,
190 | };
191 | };
192 | ```
193 |
194 |
195 | The components stop function is useful for when you want to disconnect from an external service or release some other kind of resource. The start and stop functions support both promises and callbacks (not shown)
196 |
197 | There are out of the box components for [express](https://github.com/onebeyond/systemic-express), [mongodb](https://github.com/onebeyond/systemic-mongodb), [redis](https://github.com/onebeyond/systemic-redis), [postgres](https://github.com/onebeyond/systemic-pg) and [rabbitmq](https://github.com/onebeyond/systemic-rabbitmq).
198 |
199 | #### Dependencies
200 |
201 | A component's dependencies must be registered with the system
202 |
203 |
204 | ```js
205 | const Systemic = require('systemic');
206 | const Config = require('./components/config');
207 | const Logger = require('./components/logger');
208 | const Mongo = require('./components/mongo');
209 |
210 | module.exports = () => Systemic()
211 | .add('config', Config())
212 | .add('logger', Logger())
213 | .dependsOn('config')
214 | .add('mongo', Mongo())
215 | .dependsOn('config', 'logger');
216 | ```
217 |
218 |
219 | The components dependencies are injected via it's start function
220 |
221 |
222 | ```js
223 | async function start({ config }) {
224 | db = await MongoClient.connect(config.url);
225 | return db;
226 | }
227 | ```
228 |
229 |
230 | #### Mapping dependencies
231 |
232 | You can rename dependencies passed to a components start function by specifying a mapping object instead of a simple string
233 |
234 |
235 | ```js
236 | module.exports = () => Systemic()
237 | .add('config', Config())
238 | .add('mongo', Mongo())
239 | .dependsOn({ component: 'config', destination: 'options' });
240 | ```
241 |
242 |
243 | If you want to inject a property or subdocument of the dependency thing you can also express this with a dependency mapping
244 |
245 |
246 | ```js
247 | module.exports = () => Systemic()
248 | .add('config', Config())
249 | .add('mongo', Mongo())
250 | .dependsOn({ component: 'config', source: 'config.mongo' });
251 | ```
252 |
253 |
254 | Now `config.mongo` will be injected as `config` instead of the entire configuration object
255 |
256 | #### Scoped Dependencies
257 |
258 | Injecting a sub document from a json configuration file is such a common use case, you can enable this behaviour automatically by 'scoping' the component. The following code is equivalent to that above
259 |
260 |
261 | ```js
262 | module.exports = () => Systemic()
263 | .add('config', Config(), { scoped: true })
264 | .add('mongo', Mongo())
265 | .dependsOn('config');
266 | ```
267 |
268 |
269 | #### Optional Dependencies
270 |
271 | By default an error is thrown if a dependency is not available on system start. Sometimes a component might have an optional dependency on a component they may or may not be available in the system, typically when using subsystems. In this situation a dependency can be marked as optional.
272 |
273 |
274 | ```js
275 | module.exports = () => Systemic()
276 | .add('app', app())
277 | .add('server', server())
278 | .dependsOn('app', { component: 'routes', optional: true });
279 | ```
280 |
281 |
282 | #### Overriding Components
283 |
284 | Attempting to add the same component twice will result in an error, but sometimes you need to replace existing components with test doubles. Under such circumstances use `set` instead of `add`
285 |
286 |
287 | ```js
288 | const System = require('../lib/system');
289 | const stub = require('./stubs/store');
290 |
291 | let testSystem;
292 |
293 | before(async () => {
294 | testSystem = System().set('store', stub);
295 | await testSystem.start();
296 | });
297 |
298 | after(async () => {
299 | await testSystem.stop();
300 | });
301 | ```
302 |
303 |
304 | #### Removing Components
305 |
306 | Removing components during tests can decrease startup time
307 |
308 |
309 | ```js
310 | const System = require('../lib/system');
311 |
312 | let testSystem;
313 |
314 | before(async () => {
315 | testSystem = System().remove('server');
316 | await testSystem.start();
317 | });
318 |
319 | after(async () => {
320 | await testSystem.stop();
321 | });
322 | ```
323 |
324 |
325 | #### Including components from another system
326 |
327 | You can simplify large systems by breaking them up into smaller ones, then including their component definitions into the main system.
328 |
329 |
330 | ```js
331 | // db-system.js
332 | const Systemic = require('systemic');
333 | const Mongo = require('./components/mongo');
334 |
335 | module.exports = () => Systemic()
336 | .add('mongo', Mongo())
337 | .dependsOn('config', 'logger');
338 | ```
339 |
340 |
341 |
342 | ```js
343 | // system.js
344 | const Systemic = require('systemic');
345 | const UtilSystem = require('./lib/util/system');
346 | const WebSystem = require('./lib/web/system');
347 | const DbSystem = require('./lib/db/system');
348 |
349 | module.exports = () => Systemic().include(UtilSystem()).include(WebSystem()).include(DbSystem());
350 | ```
351 |
352 |
353 | #### Grouping components
354 |
355 | Sometimes it's convenient to depend on a group of components. e.g.
356 |
357 |
358 | ```js
359 | module.exports = () => Systemic()
360 | .add('app', app())
361 | .add('routes.admin', adminRoutes())
362 | .dependsOn('app')
363 | .add('routes.api', apiRoutes())
364 | .dependsOn('app')
365 | .add('routes')
366 | .dependsOn('routes.admin', 'routes.api')
367 | .add('server')
368 | .dependsOn('app', 'routes');
369 | ```
370 |
371 |
372 | The above example will create a component 'routes', which will depend on routes.admin and routes.api and be injected as
373 |
374 |
375 | ```js
376 | {
377 | routes: {
378 | admin: { ... },
379 | adpi: { ... }
380 | }
381 | }
382 | ```
383 |
384 |
385 | #### Bootstrapping components
386 |
387 | The dependency graph for a medium size project can grow quickly leading to a large system definition. To simplify this you can bootstrap components from a specified directory, where each folder in the directory includes an index.js which defines a sub system. e.g.
388 |
389 | ```
390 |
391 | lib/
392 | |- system.js
393 | |- components/
394 | |- config/
395 | |- index.js
396 | |- logging/
397 | |- index.js
398 | |- express/
399 | |- index.js
400 | |- routes/
401 | |- admin-routes.js
402 | |- api-routes.js
403 | |- index.js
404 | ```
405 |
406 |
407 |
408 |
409 | ```js
410 | // system.js
411 | const Systemic = require('systemic');
412 | const path = require('path');
413 |
414 | module.exports = () => Systemic()
415 | .bootstrap(path.join(__dirname, 'components'));
416 | ```
417 |
418 |
419 |
420 | ```js
421 | // components/routes/index.js
422 | const Systemic = require('systemic');
423 | const adminRoutes = require('./admin-routes');
424 | const apiRoutes = require('./api-routes');
425 |
426 | module.exports = () => Systemic()
427 | .add('routes.admin', adminRoutes())
428 | .dependsOn('app')
429 | .add('routes.api', apiRoutes())
430 | .dependsOn('app', 'mongodb')
431 | .add('routes')
432 | .dependsOn('routes.admin', 'routes.api');
433 | ```
434 |
435 |
436 | ### Debugging
437 |
438 | You can debug systemic by setting the DEBUG environment variable to `systemic:*`. Naming your systems will make reading the debug output easier when you have more than one.
439 |
440 |
441 | ```js
442 | // system.js
443 | const Systemic = require('systemic');
444 | const path = require('path');
445 |
446 | module.exports = () => Systemic({ name: 'server' })
447 | .bootstrap(path.join(__dirname, 'components'));
448 | ```
449 |
450 |
451 |
452 | ```js
453 | // components/routes/index.js
454 | import Systemic from 'systemic';
455 | import adminRoutes from './admin-routes';
456 | import apiRoutes from './api-routes';
457 |
458 | export default Systemic({ name: 'routes' })
459 | .add('routes.admin', adminRoutes())
460 | .add('routes.api', apiRoutes())
461 | .add('routes')
462 | .dependsOn('routes.admin', 'routes.api');
463 | ```
464 |
465 |
466 | ```
467 | DEBUG='systemic:*' node system
468 | systemic:index Adding component routes.admin to system routes +0ms
469 | systemic:index Adding component routes.api to system auth +2ms
470 | systemic:index Adding component routes to system auth +1ms
471 | systemic:index Including definitions from sub system routes into system server +0ms
472 | systemic:index Starting system server +0ms
473 | systemic:index Inspecting component routes.admin +0ms
474 | systemic:index Starting component routes.admin +0ms
475 | systemic:index Component routes.admin started +15ms
476 | systemic:index Inspecting component routes.api +0ms
477 | systemic:index Starting component routes.api +0ms
478 | systemic:index Component routes.api started +15ms
479 | systemic:index Inspecting component routes +0ms
480 | systemic:index Injecting dependency routes.admin as routes.admin into routes +0ms
481 | systemic:index Injecting dependency routes.api as routes.api into routes +0ms
482 | systemic:index Starting component routes +0ms
483 | systemic:index Component routes started +15ms
484 | systemic:index Injecting dependency routes as routes into server +1ms
485 | systemic:index System server started +15ms
486 | ```
487 |
488 | ## Contributors ✨
489 |
490 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
491 |
492 |
493 |
494 |
495 |
502 |
503 |
504 |
505 |
506 |
507 |
508 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
509 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
2 |
--------------------------------------------------------------------------------
/assets/banner-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/banner-transparent.png
--------------------------------------------------------------------------------
/assets/banner-transparent.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/banner.png
--------------------------------------------------------------------------------
/assets/banner.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | systemic - A minimal dependency injection library for node
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/config/config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start(dependencies) {
3 | return {
4 | logger: {
5 | level: 'warn',
6 | },
7 | mongo: {
8 | primary: {
9 | url: 'mongo://primary',
10 | },
11 | secondary: {
12 | url: 'mongo://secondary',
13 | },
14 | },
15 | };
16 | }
17 |
18 | return {
19 | start: start,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/config/index.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../../../..');
2 | const Config = require('./config');
3 |
4 | module.exports = () => Systemic({ name: 'config' }).add('config', Config(), { scoped: true });
5 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/logger/index.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../../../..');
2 | const Logger = require('./logger');
3 |
4 | module.exports = () => Systemic({ name: 'logger' }).add('logger', Logger()).dependsOn('config');
5 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/logger/logger.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start({ config }) {
3 | console.log('Logging at level', config.level);
4 | return console;
5 | }
6 |
7 | return {
8 | start: start,
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/mongo/index.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../../../..');
2 | const Mongo = require('./mongo');
3 |
4 | module.exports = () =>
5 | Systemic({ name: 'mongo' })
6 | .add('mongo.primary', Mongo())
7 | .dependsOn('config', 'logger')
8 | .add('mongo.secondary', Mongo())
9 | .dependsOn('config', 'logger');
10 |
--------------------------------------------------------------------------------
/examples/bootstrapped/components/mongo/mongo.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start({ logger, config }) {
3 | logger.info('Connecting to', config.url);
4 | return {};
5 | }
6 |
7 | return {
8 | start: start,
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/examples/bootstrapped/index.js:
--------------------------------------------------------------------------------
1 | const System = require('./system');
2 |
3 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };
4 |
5 | async function start() {
6 | const system = System();
7 | const { config, mongo, logger } = await system.start();
8 |
9 | console.log('System has started. Press CTRL+C to stop');
10 |
11 | Object.keys(events).forEach((name) => {
12 | process.on(name, async () => {
13 | await system.stop();
14 | console.log('System has stopped');
15 | process.exit(events[name]);
16 | });
17 | });
18 | }
19 |
20 | start();
21 |
22 | setInterval(() => Number.MAX_INT_VALUE);
23 |
--------------------------------------------------------------------------------
/examples/bootstrapped/system.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../..');
2 | const path = require('path');
3 | const components = path.join(__dirname, 'components');
4 |
5 | module.exports = () => Systemic({ name: 'main' }).bootstrap(components);
6 |
--------------------------------------------------------------------------------
/examples/simple/components/config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start(dependencies) {
3 | return {
4 | logger: {
5 | level: 'warn',
6 | },
7 | mongo: {
8 | primary: {
9 | url: 'mongo://primary',
10 | },
11 | secondary: {
12 | url: 'mongo://secondary',
13 | },
14 | },
15 | };
16 | }
17 |
18 | return {
19 | start: start,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/examples/simple/components/logger.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start({ config }) {
3 | console.log('Logging at level', config.level);
4 | return console;
5 | }
6 |
7 | return {
8 | start: start,
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/examples/simple/components/mongo.js:
--------------------------------------------------------------------------------
1 | module.exports = function (options) {
2 | async function start({ logger, config }) {
3 | return logger.info('Connecting to', config.url);
4 | }
5 |
6 | return {
7 | start: start,
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/examples/simple/index.js:
--------------------------------------------------------------------------------
1 | const System = require('./system');
2 |
3 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };
4 |
5 | async function start() {
6 | const system = System();
7 | const { config, mongo, logger } = await system.start();
8 |
9 | console.log('System has started. Press CTRL+C to stop');
10 |
11 | Object.keys(events).forEach((name) => {
12 | process.on(name, async () => {
13 | await system.stop();
14 | console.log('System has stopped');
15 | process.exit(events[name]);
16 | });
17 | });
18 | }
19 |
20 | start();
21 |
22 | setInterval(() => Number.MAX_INT_VALUE);
23 |
--------------------------------------------------------------------------------
/examples/simple/system.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../..');
2 | const Config = require('./components/config');
3 | const Logger = require('./components/logger');
4 | const Mongo = require('./components/mongo');
5 |
6 | module.exports = () =>
7 | Systemic()
8 | .add('config', Config(), { scoped: true })
9 | .add('logger', Logger())
10 | .dependsOn('config')
11 | .add('mongo.primary', Mongo())
12 | .dependsOn('config', 'logger')
13 | .add('mongo.secondary', Mongo())
14 | .dependsOn('config', 'logger');
15 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | type RequiredKeys = {
2 | [K in keyof T]-?: {} extends Pick ? never : K;
3 | }[keyof T];
4 |
5 | type NameToDestination = TOption extends {
6 | component: infer Component;
7 | destination?: infer Destination;
8 | }
9 | ? unknown extends Destination
10 | ? Component
11 | : Destination
12 | : TOption extends string | number | symbol
13 | ? TOption
14 | : never;
15 |
16 | type MissingDependencies, TNames extends unknown[]> = TNames extends [
17 | infer Name,
18 | ...infer Rest
19 | ]
20 | ? NameToDestination extends keyof TDependencies
21 | ? MissingDependencies>, Rest>
22 | : MissingDependencies
23 | : TDependencies;
24 |
25 | /**
26 | * Systemic component that can be added to the systemic system.
27 | * @template TComponent The type of the component that will be exposed by the systemic system
28 | * @template TDependencies The type of the dependencies this component depends on
29 | */
30 | export type Component = {}> = {
31 | /**
32 | * Starts this component
33 | * @param {TDependencies} dependencies The dependencies of this component
34 | * @returns A started component
35 | */
36 | start: (dependencies: TDependencies) => Promise;
37 | /**
38 | * Stops this component
39 | */
40 | stop?: () => Promise;
41 | };
42 |
43 | /**
44 | * Systemic component that can be added to the systemic system.
45 | * @template TComponent The type of the component that will be exposed by the systemic system
46 | * @template TDependencies The type of the dependencies this component depends on
47 | */
48 | export type CallbackComponent = {}> = {
49 | /**
50 | * Starts this component
51 | * @param {TDependencies} dependencies The dependencies of this component
52 | * @param callback Callback receives the component after it has been built
53 | */
54 | start: (dependencies: TDependencies, callback: (err: any, component: TComponent) => void) => void;
55 | /**
56 | * Stops this component
57 | * @param callback Callback is called when the component has been stopped
58 | */
59 | stop?: (callback: (err?: any) => void) => void;
60 | };
61 |
62 | type SimpleDependsOnOption = keyof TSystemic;
63 | type MappingDependsOnOption = TDependencyKeys extends keyof TSystemic
64 | ? {
65 | component: keyof TSystemic;
66 | destination?: TDependencyKeys;
67 | optional?: boolean;
68 | source?: string;
69 | }
70 | : {
71 | component: keyof TSystemic;
72 | destination: TDependencyKeys;
73 | optional?: boolean;
74 | source?: string;
75 | };
76 | type DependsOnOption =
77 | | SimpleDependsOnOption
78 | | MappingDependsOnOption;
79 |
80 | type DependsOn, TDependencies extends Record> = {
81 | /**
82 | * Specifies which other components the last added components depends on.
83 | * When name and type of the dependencies match those available in the system, the dependency can be added by name.
84 | * When a dependency is named differently in the system or only part of a component is required as a dependency, a MappingDependsOnOption can be used.
85 | */
86 | dependsOn: []>(
87 | ...names: TNames
88 | ) => SystemicBuild>;
89 | };
90 |
91 | type SystemicBuild, TDependencies extends Record> = [
92 | RequiredKeys
93 | ] extends [never]
94 | ? Systemic & DependsOn
95 | : DependsOn;
96 |
97 | /**
98 | * Systemic system.
99 | */
100 | export type Systemic> = {
101 | /**
102 | * The name of the system
103 | */
104 | name: string;
105 |
106 | /**
107 | * Adds a component to the system
108 | * @param {string} name the name under which the component will be registered in the system
109 | * @param {Component} component the component to be added
110 | * @param options registration options
111 | */
112 | add: = {}>(
113 | name: S extends keyof T ? never : S, // We don't allow duplicate names
114 | component?: Component | CallbackComponent | TComponent,
115 | options?: { scoped?: boolean }
116 | ) => SystemicBuild<
117 | {
118 | [G in keyof T | S]: G extends keyof T ? T[G] : TComponent;
119 | },
120 | TDependencies
121 | >;
122 |
123 | /**
124 | * Attempting to add the same component twice will result in an error, but sometimes you need to replace existing components with test doubles. Under such circumstances use set instead of add.
125 | * @param {string} name the name under which the component will be registered in the system
126 | * @param {Component} component the component to be added
127 | * @param options registration options
128 | */
129 | set: = {}>(
130 | name: S,
131 | component: Component | CallbackComponent | TComponent,
132 | options?: { scoped?: boolean }
133 | ) => SystemicBuild<
134 | {
135 | [G in keyof T | S]: G extends keyof T ? T[G] : TComponent;
136 | },
137 | TDependencies
138 | >;
139 |
140 | /**
141 | * Adds a configuration to the system, which will be available as a scoped dependency named 'config'
142 | */
143 | configure: = {}>(
144 | component: Component | CallbackComponent | TComponent
145 | ) => SystemicBuild;
146 |
147 | /**
148 | * Removes a component from the system.
149 | * Removing components during tests can decrease startup time.
150 | */
151 | remove: (name: S) => Systemic>;
152 |
153 | /**
154 | * Includes a subsystem into this systemic system
155 | */
156 | merge: >(subSystem: Systemic) => Systemic;
157 |
158 | /**
159 | * Includes a subsystem into this systemic system
160 | */
161 | include: >(subSystem: Systemic) => Systemic;
162 |
163 | /**
164 | * Starts the system and all of its components
165 | */
166 | start(callback: (error: Error | null, result?: T) => void): void;
167 | start(): Promise;
168 |
169 | /**
170 | * Stops the system and all of its components
171 | */
172 | stop(callback: (error: Error | null) => void): void;
173 | stop(): Promise;
174 |
175 | /**
176 | * Restarts the system and all of its components
177 | */
178 | restart(callback: (error: Error | null, result?: T) => void): void;
179 | restart(): Promise;
180 |
181 | /**
182 | * The dependency graph for a medium size project can grow quickly leading to a large system definition.
183 | * To simplify this you can bootstrap components from a specified directory, where each folder in the directory includes an index.js which defines a sub system. e.g.
184 | * See documentation for more details.
185 | */
186 | bootstrap: = Record>(path: string) => Systemic;
187 | };
188 |
189 | /**
190 | * Creates a system to which components for dependency injection can be added
191 | * @returns An empty systemic system
192 | */
193 | declare function Systemic = {}>(options?: { name?: string }): Systemic;
194 |
195 | export default Systemic;
196 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const async = require('async');
2 | const debug = require('debug')('systemic:index');
3 | const { format } = require('util');
4 | const Toposort = require('toposort-class');
5 | const requireAll = require('require-all');
6 | const { randomName, isFunction, arraysIntersection, hasProp, getProp, setProp } = require('./utils');
7 |
8 | module.exports = function (_params) {
9 | const api = {};
10 | const params = Object.assign({}, { name: randomName() }, _params);
11 | let definitions = {};
12 | let currentDefinition;
13 | let running = false;
14 | let started;
15 | const defaultComponent = {
16 | start(dependencies, cb) {
17 | cb(null, dependencies);
18 | },
19 | };
20 |
21 | function bootstrap(path) {
22 | requireAll({
23 | dirname: path,
24 | filter: /^(index.js)$/,
25 | resolve(exported) {
26 | const component = exported.default || exported;
27 | api.include(isFunction(component) ? component() : component);
28 | },
29 | });
30 | return api;
31 | }
32 |
33 | function configure(component) {
34 | return add('config', component, { scoped: true });
35 | }
36 |
37 | function add(...args) {
38 | const [name, component, options] = args;
39 | debug('Adding component %s to system %s', name, params.name);
40 | if (definitions.hasOwnProperty(name)) throw new Error(format('Duplicate component: %s', name));
41 | if (args.length === 1) return add(name, defaultComponent);
42 | return _set(name, component, options);
43 | }
44 |
45 | function set(name, component, options) {
46 | debug('Setting component %s on system %s', name, params.name);
47 | return _set(name, component, options);
48 | }
49 |
50 | function remove(name) {
51 | debug('Removing component %s from system %s', name, params.name);
52 | delete definitions[name];
53 | return api;
54 | }
55 |
56 | function _set(name, component, options) {
57 | if (!component) throw new Error(format('Component %s is null or undefined', name));
58 | definitions[name] = Object.assign({}, options, {
59 | name,
60 | component: component.start ? component : wrap(component),
61 | dependencies: [],
62 | });
63 | currentDefinition = definitions[name];
64 | return api;
65 | }
66 |
67 | function include(subSystem) {
68 | debug('Including definitions from sub system %s into system %s', subSystem.name, params.name);
69 | definitions = Object.assign({}, definitions, subSystem._definitions);
70 | return api;
71 | }
72 |
73 | function dependsOn(...args) {
74 | if (!currentDefinition) throw new Error('You must add a component before calling dependsOn');
75 | currentDefinition.dependencies = args.reduce(toDependencyDefinitions, currentDefinition.dependencies);
76 | return api;
77 | }
78 |
79 | function toDependencyDefinitions(accumulator, arg) {
80 | const record =
81 | typeof arg === 'string'
82 | ? {
83 | component: arg,
84 | destination: arg,
85 | optional: false,
86 | }
87 | : Object.assign({}, { destination: arg.component }, arg);
88 | if (!record.component)
89 | throw new Error(format('Component %s has an invalid dependency %s', currentDefinition.name, JSON.stringify(arg)));
90 | if (currentDefinition.dependencies.find((dep) => dep.destination === record.destination)) {
91 | throw new Error(format('Component %s has a duplicate dependency %s', currentDefinition.name, record.destination));
92 | }
93 | return accumulator.concat(record);
94 | }
95 |
96 | function start(cb) {
97 | debug('Starting system %s', params.name);
98 | started = [];
99 | const p = new Promise((resolve, reject) => {
100 | async.seq(sortComponents, ensureComponents, (components, cb) => {
101 | debug('System %s started', params.name);
102 | running = components;
103 | cb(null, components);
104 | })((err, components) => {
105 | if (err) return reject(err, {});
106 | resolve(components);
107 | });
108 | });
109 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb, {})) : p;
110 | }
111 |
112 | function ensureComponents(components, cb) {
113 | if (running) return cb(null, running);
114 | async.reduce(components.reverse(), {}, toSystem, cb);
115 | }
116 |
117 | function toSystem(system, name, cb) {
118 | debug('Inspecting compontent %s', name);
119 | getDependencies(name, system, (err, dependencies) => {
120 | if (err) return cb(err);
121 | startComponent(dependencies, name, system, cb);
122 | });
123 | }
124 |
125 | function startComponent(dependencies, name, system, cb) {
126 | debug('Starting component %s', name);
127 | started.push(name);
128 | const { component } = definitions[name];
129 | const onStarted = (err, c) => {
130 | if (err) return cb(err);
131 | setProp(system, name, c);
132 | debug('Component %s started', name);
133 | setImmediate(() => {
134 | cb(null, system);
135 | });
136 | };
137 | const p = component.start(dependencies, onStarted);
138 | if (p && p.then) {
139 | p.then(immediateCallback(onStarted)).catch(immediateError(cb));
140 | }
141 | }
142 |
143 | function stop(cb) {
144 | debug('Stopping system %s', params.name);
145 | const p = new Promise((resolve, reject) => {
146 | async.seq(sortComponents, removeUnstarted, stopComponents, (cb) => {
147 | debug('System %s stopped', params.name);
148 | running = false;
149 | cb();
150 | })((err) => {
151 | if (err) return reject(err);
152 | resolve();
153 | });
154 | });
155 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb)) : p;
156 | }
157 |
158 | function stopComponents(components, cb) {
159 | async.eachSeries(components, stopComponent, cb);
160 | }
161 |
162 | function stopComponent(name, cb) {
163 | debug('Stopping component %s', name);
164 | const stopFn = definitions[name].component.stop || noop;
165 | const onStopped = (err) => {
166 | if (err) return cb(err);
167 | debug('Component %s stopped', name);
168 | setImmediate(cb);
169 | };
170 | const p = stopFn(onStopped);
171 | if (p && p.then) {
172 | p.then(immediateCallback(onStopped)).catch(immediateError(cb));
173 | }
174 | }
175 |
176 | function sortComponents(cb) {
177 | let result = [];
178 | try {
179 | const graph = new Toposort();
180 | Object.keys(definitions).forEach((name) => {
181 | graph.add(
182 | name,
183 | definitions[name].dependencies.map((dep) => dep.component)
184 | );
185 | });
186 | result = arraysIntersection(graph.sort(), Object.keys(definitions));
187 | } catch (err) {
188 | return cb(err);
189 | }
190 | return cb(null, result);
191 | }
192 |
193 | function removeUnstarted(components, cb) {
194 | cb(null, arraysIntersection(components, started));
195 | }
196 |
197 | function getDependencies(name, system, cb) {
198 | async.reduce(
199 | definitions[name].dependencies,
200 | {},
201 | (accumulator, dependency, cb) => {
202 | if (!hasProp(definitions, dependency.component) && !dependency.optional)
203 | return cb(new Error(format('Component %s has an unsatisfied dependency on %s', name, dependency.component)));
204 | if (!hasProp(definitions, dependency.component)) {
205 | debug('Skipping unsatisfied optional dependency %s for component %s', dependency.component, name);
206 | return cb(null, accumulator);
207 | }
208 | if (!dependency.hasOwnProperty('source') && definitions[dependency.component].scoped) dependency.source = name;
209 | dependency.source
210 | ? debug(
211 | 'Injecting dependency %s.%s as %s into %s',
212 | dependency.component,
213 | dependency.source,
214 | dependency.destination,
215 | name
216 | )
217 | : debug('Injecting dependency %s as %s into %s', dependency.component, dependency.destination, name);
218 | const component = getProp(system, dependency.component);
219 | setProp(
220 | accumulator,
221 | dependency.destination,
222 | dependency.source ? getProp(component, dependency.source) : component
223 | );
224 | cb(null, accumulator);
225 | },
226 | cb
227 | );
228 | }
229 |
230 | function noop(...args) {
231 | const cb = args.pop();
232 | cb && cb(...[null].concat(args));
233 | }
234 |
235 | function wrap(component) {
236 | return {
237 | start(dependencies, cb) {
238 | return cb(null, component);
239 | },
240 | };
241 | }
242 |
243 | function restart(cb) {
244 | const p = api.stop().then(() => api.start());
245 |
246 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb)) : p;
247 | }
248 |
249 | function immediateCallback(cb) {
250 | return (resolved) => {
251 | setImmediate(() => {
252 | cb(null, resolved);
253 | });
254 | };
255 | }
256 |
257 | function immediateError(cb, resolved) {
258 | return (err) => {
259 | setImmediate(() => {
260 | resolved ? cb(err, resolved) : cb(err);
261 | });
262 | };
263 | }
264 |
265 | Object.assign(api, {
266 | name: params.name,
267 | bootstrap,
268 | configure,
269 | add,
270 | set,
271 | remove,
272 | merge: include,
273 | include,
274 | dependsOn,
275 | start,
276 | stop,
277 | restart,
278 | _definitions: definitions,
279 | });
280 |
281 | return api;
282 | };
283 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "systemic",
3 | "version": "4.1.2",
4 | "description": "A minimal dependency injection library for node",
5 | "main": "index.js",
6 | "typings": "index.d.ts",
7 | "scripts": {
8 | "lint": "eslint .",
9 | "lint:fix": "eslint . --fix",
10 | "prettier": "prettier . --check",
11 | "prettier:fix": "prettier . --write",
12 | "test": "zUnit",
13 | "coverage": "nyc --report html --reporter lcov --reporter text-summary zUnit",
14 | "prepare": "husky install",
15 | "release:prerelease": "npm run release -- prerelease"
16 | },
17 | "keywords": [
18 | "dependency",
19 | "injection",
20 | "context",
21 | "inversion of control",
22 | "graceful",
23 | "start up",
24 | "shutdown",
25 | "ioc",
26 | "boot"
27 | ],
28 | "author": "GuideSmiths Ltd",
29 | "license": "MIT",
30 | "devDependencies": {
31 | "eslint": "^8.11.0",
32 | "eslint-config-airbnb-base": "^15.0.0",
33 | "eslint-config-prettier": "^8.5.0",
34 | "eslint-plugin-import": "^2.26.0",
35 | "husky": "^7.0.4",
36 | "lint-staged": "^12.3.5",
37 | "nyc": "^15.1.0",
38 | "prettier": "2.5.1",
39 | "zunit": "^3.2.1"
40 | },
41 | "dependencies": {
42 | "async": "^3.2.3",
43 | "debug": "^4.3.4",
44 | "require-all": "^3.0.0",
45 | "toposort-class": "^1.0.1"
46 | },
47 | "directories": {
48 | "example": "examples"
49 | },
50 | "engines": {
51 | "node": ">=12.0.0"
52 | },
53 | "lint-staged": {
54 | "**/*": "prettier --write --ignore-unknown",
55 | "**/*.js": "eslint --fix"
56 | },
57 | "repository": {
58 | "type": "git",
59 | "url": "git+https://github.com/guidesmiths/systemic.git"
60 | },
61 | "bugs": {
62 | "url": "https://github.com/guidesmiths/systemic/issues"
63 | },
64 | "homepage": "https://guidesmiths.github.io/systemic/",
65 | "husky": {
66 | "hooks": {
67 | "pre-commit": "npm run qa"
68 | }
69 | },
70 | "zUnit": {
71 | "pollute": true
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "describe": "readonly",
4 | "xdescribe": "readonly",
5 | "odescribe": "readonly",
6 | "it": "readonly",
7 | "xit": "readonly",
8 | "oit": "readonly",
9 | "before": "readonly",
10 | "beforeEach": "readonly",
11 | "after": "readonly",
12 | "afterEach": "readonly",
13 | "include": "readonly"
14 | },
15 | "rules": {
16 | "no-shadow": ["error", { "allow": ["err", "cb", "components"] }]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/test/components/bar/index.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../../..');
2 |
3 | module.exports = () => Systemic().add('bar').dependsOn('foo');
4 |
--------------------------------------------------------------------------------
/test/components/foo/index.js:
--------------------------------------------------------------------------------
1 | const Systemic = require('../../..');
2 |
3 | module.exports = () => Systemic().add('foo');
4 |
--------------------------------------------------------------------------------
/test/systemic.test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const path = require('path');
3 | const System = require('..');
4 |
5 | describe('System', () => {
6 | let system;
7 |
8 | beforeEach(() => {
9 | system = System();
10 | });
11 |
12 | it('should start without components', (test, done) => {
13 | system.start((err, components) => {
14 | assert.ifError(err);
15 | assert.equal(Object.keys(components).length, 0);
16 | done();
17 | });
18 | });
19 |
20 | it('should stop without components', (test, done) => {
21 | system.start((err) => {
22 | assert.ifError(err);
23 | system.stop(done);
24 | });
25 | });
26 |
27 | it('should tolerate being stopped without being started', (test, done) => {
28 | system.stop(done);
29 | });
30 |
31 | it('should tolerate being started wthout being stopped', (test, done) => {
32 | system.add('foo', new CallbackComponent());
33 | system.start((err, components) => {
34 | assert.ifError(err);
35 | assert.equal(components.foo.counter, 1);
36 | system.start((err, components) => {
37 | assert.ifError(err);
38 | assert.equal(components.foo.counter, 1);
39 | done();
40 | });
41 | });
42 | });
43 |
44 | it('should restart', (test, done) => {
45 | system.add('foo', new CallbackComponent());
46 | system.start((err, components) => {
47 | assert.ifError(err);
48 | assert.equal(components.foo.counter, 1);
49 | system.restart((err, components) => {
50 | assert.ifError(err);
51 | assert.equal(components.foo.counter, 2);
52 | done();
53 | });
54 | });
55 | });
56 |
57 | it('should start callback components', (test, done) => {
58 | system.add('foo', new CallbackComponent()).start((err, components) => {
59 | assert.ifError(err);
60 | assert(components.foo.started, 'Component was not started');
61 | done();
62 | });
63 | });
64 |
65 | it('should stop callback components', (test, done) => {
66 | system.add('foo', new CallbackComponent()).start((err, components) => {
67 | assert.ifError(err);
68 | system.stop((err) => {
69 | assert.ifError(err);
70 | assert(components.foo.stopped, 'Component was not stopped');
71 | done();
72 | });
73 | });
74 | });
75 |
76 | it('should start promise components', (test, done) => {
77 | system.add('foo', new PromiseComponent()).start((err, components) => {
78 | assert.ifError(err);
79 | assert(components.foo.started, 'Component was not started');
80 | done();
81 | });
82 | });
83 |
84 | it('should stop promise components', (test, done) => {
85 | system.add('foo', new PromiseComponent()).start((err, components) => {
86 | assert.ifError(err);
87 | system.stop((err) => {
88 | assert.ifError(err);
89 | assert(components.foo.stopped, 'Component was not stopped');
90 | done();
91 | });
92 | });
93 | });
94 |
95 | it('should not stop components that werent started', (test, done) => {
96 | const bar = new CallbackComponent();
97 | system
98 | .add('foo', new ErrorCallbackComponent())
99 | .add('bar', bar)
100 | .dependsOn('foo')
101 | .start((err) => {
102 | assert.ok(err);
103 | system.stop((err) => {
104 | assert.ifError(err);
105 | assert(!bar.stopped, 'Component was stopped');
106 | done();
107 | });
108 | });
109 | });
110 |
111 | it('should tolerate when a callback component errors', (test, done) => {
112 | const bar = new CallbackComponent();
113 | system
114 | .add('foo', new ErrorCallbackComponent())
115 | .add('bar', bar)
116 | .dependsOn('foo')
117 | .start((err, components) => {
118 | assert.ok(err);
119 | assert.deepEqual(components, {});
120 | done();
121 | });
122 | });
123 |
124 | it('should tolerate when a promise component errors', (test, done) => {
125 | const bar = new PromiseComponent();
126 | system
127 | .add('foo', new ErrorPromiseComponent())
128 | .add('bar', bar)
129 | .dependsOn('foo')
130 | .start((err, components) => {
131 | assert.ok(err);
132 | assert.deepEqual(components, {});
133 | done();
134 | });
135 | });
136 |
137 | it('should pass through components without start methods', (test, done) => {
138 | system.add('foo', { ok: true }).start((err, components) => {
139 | assert.ifError(err);
140 | assert.equal(components.foo.ok, true);
141 | done();
142 | });
143 | });
144 |
145 | it('should tolerate components without stop methods', (test, done) => {
146 | system.add('foo', new Unstoppable()).start((err, components) => {
147 | assert.ifError(err);
148 | system.stop((err) => {
149 | assert.ifError(err);
150 | assert(components.foo.stopped, 'Component was not stopped');
151 | done();
152 | });
153 | });
154 | });
155 |
156 | it('should reject duplicate components', () => {
157 | assert.throws(
158 | () => {
159 | system.add('foo', new CallbackComponent()).add('foo', new CallbackComponent());
160 | },
161 | { message: 'Duplicate component: foo' }
162 | );
163 | });
164 |
165 | it('should reject attempts to add an undefined component', () => {
166 | assert.throws(
167 | () => {
168 | system.add('foo', undefined);
169 | },
170 | { message: 'Component foo is null or undefined' }
171 | );
172 | });
173 |
174 | it('should reject dependsOn called before adding components', () => {
175 | assert.throws(
176 | () => {
177 | system.dependsOn('foo');
178 | },
179 | { message: 'You must add a component before calling dependsOn' }
180 | );
181 | });
182 |
183 | it('should report missing dependencies', (test, done) => {
184 | system
185 | .add('foo', new CallbackComponent())
186 | .dependsOn('bar')
187 | .start((err) => {
188 | assert(err);
189 | assert.equal(err.message, 'Component foo has an unsatisfied dependency on bar');
190 | done();
191 | });
192 | });
193 |
194 | it('should inject dependencies', (test, done) => {
195 | system
196 | .add('bar', new CallbackComponent())
197 | .add('baz', new CallbackComponent())
198 | .add('foo', new CallbackComponent())
199 | .dependsOn('bar')
200 | .dependsOn('baz')
201 | .start((err, components) => {
202 | assert.ifError(err);
203 | assert(components.foo.dependencies.bar);
204 | assert(components.foo.dependencies.baz);
205 | done();
206 | });
207 | });
208 |
209 | it('should inject multiple dependencies expressed in a single dependsOn', (test, done) => {
210 | system
211 | .add('bar', new CallbackComponent())
212 | .add('baz', new CallbackComponent())
213 | .add('foo', new CallbackComponent())
214 | .dependsOn('bar', 'baz')
215 | .start((err, components) => {
216 | assert.ifError(err);
217 | assert(components.foo.dependencies.bar);
218 | assert(components.foo.dependencies.baz);
219 | done();
220 | });
221 | });
222 |
223 | it('should map dependencies to a new name', (test, done) => {
224 | system
225 | .add('bar', new CallbackComponent())
226 | .add('foo', new CallbackComponent())
227 | .dependsOn({ component: 'bar', destination: 'baz' })
228 | .start((err, components) => {
229 | assert.ifError(err);
230 | assert(!components.foo.dependencies.bar);
231 | assert(components.foo.dependencies.baz);
232 | done();
233 | });
234 | });
235 |
236 | it('should inject dependencies defined out of order', (test, done) => {
237 | system
238 | .add('foo', new CallbackComponent())
239 | .dependsOn('bar')
240 | .add('bar', new CallbackComponent())
241 | .start((err, components) => {
242 | assert.ifError(err);
243 | assert(components.foo.dependencies.bar);
244 | done();
245 | });
246 | });
247 |
248 | it('should support nested component names', (test, done) => {
249 | system
250 | .add('foo.bar', new CallbackComponent())
251 | .add('baz', new CallbackComponent())
252 | .dependsOn('foo.bar')
253 | .start((err, components) => {
254 | assert.ifError(err);
255 | assert(components.foo.bar.started);
256 | assert(components.baz.dependencies.foo.bar);
257 | done();
258 | });
259 | });
260 |
261 | it('should inject dependency sub documents', (test, done) => {
262 | system
263 | .add('config', new Config({ foo: { bar: 'baz' } }))
264 | .add('foo', new CallbackComponent())
265 | .dependsOn({ component: 'config', source: 'foo', destination: 'config' })
266 | .start((err, components) => {
267 | assert.ifError(err);
268 | assert(components.foo.dependencies.config.bar, 'baz');
269 | done();
270 | });
271 | });
272 |
273 | it('should accept missing optional dependency', (test, done) => {
274 | system
275 | .add('bar', new CallbackComponent())
276 | .add('foo', new CallbackComponent())
277 | .dependsOn({ component: 'bar', optional: true }, { component: 'baz', optional: true })
278 | .start((err, components) => {
279 | assert.ifError(err);
280 | assert(components.foo.dependencies.bar);
281 | assert.equal(components.foo.dependencies.baz, undefined);
282 | done();
283 | });
284 | });
285 |
286 | it('should reject invalid dependencies', () => {
287 | assert.throws(
288 | () => {
289 | System().add('foo', new CallbackComponent()).dependsOn(1);
290 | },
291 | { message: 'Component foo has an invalid dependency 1' }
292 | );
293 |
294 | assert.throws(
295 | () => {
296 | System().add('foo', new CallbackComponent()).dependsOn({});
297 | },
298 | { message: 'Component foo has an invalid dependency {}' }
299 | );
300 | });
301 |
302 | it('should reject direct cyclic dependencies', (test, done) => {
303 | system
304 | .add('foo', new CallbackComponent())
305 | .dependsOn('foo')
306 | .start((err) => {
307 | assert(err);
308 | assert(/Cyclic dependency found/.test(err.message), err.message);
309 | done();
310 | });
311 | });
312 |
313 | it('should reject indirect cyclic dependencies', (test, done) => {
314 | system
315 | .add('foo', new CallbackComponent())
316 | .dependsOn('bar')
317 | .add('bar', new CallbackComponent())
318 | .dependsOn('foo')
319 | .start((err) => {
320 | assert(err);
321 | assert(/Cyclic dependency found/.test(err.message), err.message);
322 | done();
323 | });
324 | });
325 |
326 | it('should tolerate duplicate dependencies with different destinations', (test, done) => {
327 | system
328 | .add('foo', new CallbackComponent())
329 | .dependsOn({ component: 'bar', destination: 'a' })
330 | .dependsOn({ component: 'bar', destination: 'b' })
331 | .add('bar', new CallbackComponent())
332 | .start((err, components) => {
333 | assert.ifError(err);
334 | assert(components.foo.dependencies.a);
335 | assert(components.foo.dependencies.b);
336 | done();
337 | });
338 | });
339 |
340 | it('should reject duplicate dependency implicit destinations', () => {
341 | assert.throws(
342 | () => {
343 | system.add('foo', new CallbackComponent()).dependsOn('bar').dependsOn('bar');
344 | },
345 | { message: 'Component foo has a duplicate dependency bar' }
346 | );
347 | });
348 |
349 | it('should reject duplicate dependency explicit destinations', () => {
350 | assert.throws(
351 | () => {
352 | system
353 | .add('foo', new CallbackComponent())
354 | .dependsOn({ component: 'bar', destination: 'baz' })
355 | .dependsOn({ component: 'shaz', destination: 'baz' });
356 | },
357 | { message: 'Component foo has a duplicate dependency baz' }
358 | );
359 | });
360 |
361 | it('should provide a shorthand for scoped dependencies such as config', (test, done) => {
362 | system
363 | .configure(new Config({ foo: { bar: 'baz' } }))
364 | .add('foo', new CallbackComponent())
365 | .dependsOn('config')
366 | .start((err, components) => {
367 | assert.ifError(err);
368 | assert.equal(components.foo.dependencies.config.bar, 'baz');
369 | done();
370 | });
371 | });
372 |
373 | it('should allow shorthand to be overriden', (test, done) => {
374 | system
375 | .configure(new Config({ foo: { bar: 'baz' } }))
376 | .add('foo', new CallbackComponent())
377 | .dependsOn({ component: 'config', source: '' })
378 | .start((err, components) => {
379 | assert.ifError(err);
380 | assert.equal(components.foo.dependencies.config.foo.bar, 'baz');
381 | done();
382 | });
383 | });
384 |
385 | it('should include components from other systems', (test, done) => {
386 | system.include(System().add('foo', new CallbackComponent())).start((err, components) => {
387 | assert.ifError(err);
388 | assert.ok(components.foo);
389 | done();
390 | });
391 | });
392 |
393 | it('should be able to depend on included components', (test, done) => {
394 | system
395 | .include(System().add('foo', new CallbackComponent()))
396 | .add('bar', new CallbackComponent())
397 | .dependsOn('foo')
398 | .start((err, components) => {
399 | assert.ifError(err);
400 | assert.ok(components.bar.dependencies.foo);
401 | done();
402 | });
403 | });
404 |
405 | it('should configure components from included systems', (test, done) => {
406 | system
407 | .configure(new Config({ foo: { bar: 'baz' } }))
408 | .include(System().add('foo', new CallbackComponent()).dependsOn('config'))
409 | .start((err, components) => {
410 | assert.ifError(err);
411 | assert.equal(components.foo.dependencies.config.bar, 'baz');
412 | done();
413 | });
414 | });
415 |
416 | it('should prefer components from other systems when merging', (test, done) => {
417 | system
418 | .add('foo', 1)
419 | .include(System().add('foo', 2))
420 | .start((err, components) => {
421 | assert.ifError(err);
422 | assert.equal(components.foo, 2);
423 | done();
424 | });
425 | });
426 |
427 | it('should set components for the first time', (test, done) => {
428 | system.set('foo', 1).start((err, components) => {
429 | assert.ifError(err);
430 | assert.equal(components.foo, 1);
431 | done();
432 | });
433 | });
434 |
435 | it('should replace existing components with set', (test, done) => {
436 | system
437 | .set('foo', 1)
438 | .set('foo', 2)
439 | .start((err, components) => {
440 | assert.ifError(err);
441 | assert.equal(components.foo, 2);
442 | done();
443 | });
444 | });
445 |
446 | it('should remove existing components', (test, done) => {
447 | system
448 | .set('foo', 1)
449 | .remove('foo')
450 | .start((err, components) => {
451 | assert.ifError(err);
452 | assert.equal(components.foo, undefined);
453 | done();
454 | });
455 | });
456 |
457 | it('should group components', (test, done) => {
458 | system
459 | .add('foo.one', 1)
460 | .add('foo.two', 2)
461 | .add('foo.all')
462 | .dependsOn('foo.one', 'foo.two')
463 | .start((err, components) => {
464 | assert.ifError(err);
465 | assert.equal(components.foo.one, 1);
466 | assert.equal(components.foo.two, 2);
467 | assert.equal(components.foo.all.foo.one, 1);
468 | assert.equal(components.foo.all.foo.two, 2);
469 | done();
470 | });
471 | });
472 |
473 | it('should bootstrap components from the file system', (test, done) => {
474 | system.bootstrap(path.join(__dirname, 'components')).start((err, components) => {
475 | assert.ifError(err);
476 | assert(components.foo);
477 | assert(components.bar);
478 | done();
479 | });
480 | });
481 |
482 | it('should support promises', (test, done) => {
483 | system
484 | .add('foo', new CallbackComponent())
485 | .start()
486 | .then((components) => {
487 | assert(components.foo.started, 'Component was not started');
488 | assert(components.foo.counter, 1);
489 | return components;
490 | })
491 | .then((components) =>
492 | system
493 | .stop()
494 | .then(() => {
495 | assert(components.foo.stopped, 'Component was not stopped');
496 | assert(components.foo.counter, 1);
497 | return components;
498 | })
499 | .catch(done)
500 | )
501 | .then((components) => {
502 | system
503 | .restart()
504 | .then(() => {
505 | assert(components.foo.counter, 2);
506 | })
507 | .catch(done);
508 | })
509 | .then(done)
510 | .catch(done);
511 | });
512 |
513 | function CallbackComponent() {
514 | const state = { counter: 0, started: true, stopped: true, dependencies: [] };
515 |
516 | this.start = (dependencies, cb) => {
517 | state.started = true;
518 | state.counter++;
519 | state.dependencies = dependencies;
520 | setTimeout(() => {
521 | cb(null, state);
522 | }, 100);
523 | };
524 | this.stop = (cb) => {
525 | state.stopped = true;
526 | setTimeout(() => {
527 | cb();
528 | }, 100);
529 | };
530 | }
531 |
532 | function PromiseComponent() {
533 | const state = { counter: 0, started: true, stopped: true, dependencies: [] };
534 |
535 | this.start = (dependencies) => {
536 | state.started = true;
537 | state.counter++;
538 | state.dependencies = dependencies;
539 | return new Promise((resolve) => {
540 | setTimeout(() => {
541 | resolve(state);
542 | }, 100);
543 | });
544 | };
545 | this.stop = () => {
546 | state.stopped = true;
547 | return new Promise((resolve) => {
548 | setTimeout(() => {
549 | resolve();
550 | }, 100);
551 | });
552 | };
553 | }
554 |
555 | function ErrorCallbackComponent() {
556 | this.start = (dependencies, cb) => {
557 | cb(new Error('Oh Noes!'));
558 | };
559 | }
560 |
561 | function ErrorPromiseComponent() {
562 | this.start = () =>
563 | new Promise((resolve, reject) => {
564 | reject(new Error('Oh Noes!'));
565 | });
566 | }
567 |
568 | function Unstoppable() {
569 | const state = { started: true, stopped: true, dependencies: [] };
570 |
571 | this.start = (dependencies, cb) => {
572 | state.started = true;
573 | state.dependencies = dependencies;
574 | cb(null, state);
575 | };
576 | }
577 |
578 | function Config(config) {
579 | this.start = (dependencies, cb) => {
580 | cb(null, config);
581 | };
582 | }
583 | });
584 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | function randomName() {
2 | return `Z-${Math.floor(Math.random() * 100000000) + 1}`;
3 | }
4 |
5 | function isFunction(func) {
6 | return func && typeof func === 'function';
7 | }
8 |
9 | function arraysIntersection(...arrays) {
10 | const verifiedArrays = arrays.filter((value) => Array.isArray(value));
11 | if (arrays.length === 0) return arrays;
12 | return verifiedArrays.reduce((acc, currentArray) => {
13 | currentArray.forEach((currentValue) => {
14 | if (acc.indexOf(currentValue) === -1) {
15 | if (verifiedArrays.filter((obj) => obj.indexOf(currentValue) === -1).length === 0) {
16 | acc.push(currentValue);
17 | }
18 | }
19 | });
20 | return acc;
21 | }, []);
22 | }
23 |
24 | function hasProp(obj, key) {
25 | if (obj.hasOwnProperty(key)) return true; // Some properties with '.' could fail, so we do a quick check
26 | const keyParts = key.split('.');
27 | return (
28 | !!obj &&
29 | (keyParts.length > 1 ? hasProp(obj[key.split('.')[0]], keyParts.slice(1).join('.')) : obj.hasOwnProperty(key))
30 | );
31 | }
32 |
33 | function getProp(obj, key) {
34 | if (!!obj && obj.hasOwnProperty(key)) return obj[key]; // Some properties with '.' could fail, so we do a quick check
35 | if (key.includes('.')) {
36 | const keyParts = key.split('.');
37 | return getProp(obj[keyParts[0]], keyParts.slice(1).join('.'));
38 | }
39 | }
40 |
41 | function setProp(obj, key, value) {
42 | if (!key.includes('.')) {
43 | obj[key] = value;
44 | return;
45 | }
46 | const keyParts = key.split('.');
47 | if (!obj[keyParts[0]]) obj[keyParts[0]] = {};
48 | setProp(obj[keyParts[0]], keyParts.slice(1).join('.'), value);
49 | }
50 |
51 | module.exports = {
52 | randomName,
53 | isFunction,
54 | arraysIntersection,
55 | hasProp,
56 | getProp,
57 | setProp,
58 | };
59 |
--------------------------------------------------------------------------------