{"id":16968590,"url":"https://github.com/infinitaslearning/systemic-ts","last_synced_at":"2025-04-11T23:52:53.282Z","repository":{"id":142916308,"uuid":"604822685","full_name":"infinitaslearning/systemic-ts","owner":"infinitaslearning","description":"Simple typescript dependency injection framework based on the concepts of Systemic","archived":false,"fork":false,"pushed_at":"2024-12-15T14:38:43.000Z","size":400,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-11T23:52:46.600Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/infinitaslearning.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-21T21:33:56.000Z","updated_at":"2024-12-16T07:57:19.000Z","dependencies_parsed_at":"2025-02-20T09:43:00.063Z","dependency_job_id":null,"html_url":"https://github.com/infinitaslearning/systemic-ts","commit_stats":null,"previous_names":["infinitaslearning/systemic-ts","teunmooij/systemic-ts"],"tags_count":12,"template":false,"template_full_name":"teunmooij/typescript-module-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infinitaslearning%2Fsystemic-ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infinitaslearning%2Fsystemic-ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infinitaslearning%2Fsystemic-ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infinitaslearning%2Fsystemic-ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/infinitaslearning","download_url":"https://codeload.github.com/infinitaslearning/systemic-ts/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248497851,"owners_count":21113984,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T00:12:34.666Z","updated_at":"2025-04-11T23:52:53.260Z","avatar_url":"https://github.com/infinitaslearning.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @ilpt/systemic-ts\n\nA minimal type-safe dependency injection library, based on and compatible with [systemic](https://www.npmjs.com/package/systemic).\n\n## Installation\n\n```shell\n$ npm install @ilpt/systemic-ts\n```\n## tl;dr\n\n### Define the system\n\n```typescript\nimport { systemic } from '@ilpt/systemic-ts';\nimport initConfig from './components/config';\nimport initLogger from './components/logger';\nimport initMongo from './components/mongo';\n\nexport const initSystem =  () =\u003e systemic()\n  .add('config', initConfig(), { scoped: true })\n  .add('logger', initLogger()).dependsOn('config')\n  .add('mongo.primary', initMongo()).dependsOn('config', 'logger')\n  .add('mongo.secondary', initMongo()).dependsOn('config', 'logger');\n```\n\n### Run the system\n\n```typescript\nimport { initSystem } from './system';\n\nconst events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };\n\nasync function start() {\n  const system = initSystem();\n  const { config, mongo, logger } = await system.start();\n\n  console.log('System has started. Press CTRL+C to stop');\n\n  for (const name of Object.keys(events)) {\n    process.on(name, async () =\u003e {\n      await system.stop();\n      console.log('System has stopped');\n      process.exit(events[name]);\n    });\n  }\n}\n\nstart();\n```\n\n## Why Use Dependency Injection With Node.js?\n\nNode.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).\n\nHowever when writing microservices the life cycle of an application and its dependencies is a nuisance to manage over and over again. We want 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 find that before doing anything we need to load config from remote sources, and configure loggers. This is why one uses DI.\n\nThe journey that led to [@ilpt/systemic-ts](https://www.npmjs.com/package/@ilpt/systemic-ts) started with a dependency injection framework called [electrician](https://www.npmjs.com/package/electrician) by our friends at Tes. It served its purpose well, but the API had a couple of limitations that they wanted to fix. This would have required a backwards incompatible change, so instead a new DI library was written - [systemic](https://www.npmjs.com/package/systemic). In late 2021 an attempt was made to add typescript definitions, but the types where incomplete and difficult to debug. This is why [Teun Mooij](https://github.com/teunmooij) decided to completely re-write the library in typescript, mostly compatible with it's predecessor, but fully type safe, which we adopted at [Infinitas Learning](https://github.com/infinitaslearning) - [@ilpt/systemic-ts](https://www.npmjs.com/package/@ilpt/systemic-ts).\n\n\n## Concepts\n\n`@ilpt/systemic-ts` has 4 main concepts\n\n1. Systems\n1. Runners\n1. Components\n1. Dependencies\n\n### Systems\n\nYou add components and their dependencies to a system. When you start the system, `@ilpt/systemic-ts` iterates through all the components, starting them in the order derived from the dependency graph. When you stop the system, `@ilpt/systemic-ts` iterates through all the components stopping them in the reverse order.\n\n```typescript\nimport { systemic } from '@ilpt/systemic-ts';\nimport initConfig from './components/config';\nimport initLogger from './components/logger';\nimport initMongo from './components/mongo';\n\nconst events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };\n\nasync function init() {\n  const system = systemic()\n    .add('config', initConfig(), { scoped: true })\n    .add('logger', initLogger()).dependsOn('config')\n    .add('mongo.primary', initMongo()).dependsOn('config', 'logger')\n    .add('mongo.secondary', initMongo()).dependsOn('config', 'logger');\n\n  const { config, mongo, logger } = await system.start();\n\n  console.log('System has started. Press CTRL+C to stop');\n\n  for (const name of Object.keys(events)) {\n    process.on(name, async () =\u003e {\n      await system.stop();\n      console.log('System has stopped');\n      process.exit(events[name]);\n    });\n  }\n}\n\ninit();\n```\n\n### Runners\n\nWhile 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.\n\n```typescript\n// system.ts\nexport const initSystem = () =\u003e systemic()\n  .add('config', initConfig())\n  .add('logger', initLogger()).dependsOn('config')\n  .add('mongo', initMongo()).dependsOn('config', 'logger');\n```\n\n```typescript\n// index.ts\nimport { initSystem } from './system';\n\nconst events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 };\n\nasync function start() {\n  const system = initSystem();\n  const { config, mongo, logger } = await system.start();\n\n  console.log('System has started. Press CTRL+C to stop');\n\n  for (const name of Object.keys(events)) {\n    process.on(name, async () =\u003e {\n      await system.stop();\n      console.log('System has stopped');\n      process.exit(events[name]);\n    });\n  }\n}\n\nstart();\n```\n\nThere are some out of the box runners that can be used in your applications or as a reference for your own custom runner\n\n1. [Service Runner](https://npmjs.com/package/@ilpt/systemic-ts-service-runner)\n\n```typescript\nimport { runner } from '@ilpt/systemic-ts-service-runner';\n\nimport system from './system';\n\nrunner(system()).start().then(components =\u003e {\n  console.log('Started');\n});\n```\n\n### Components\n\nA component is or wraps the underlying resource that makes up the system. It has optional start and stop functions. The start function should return or yield the underlying resource after it has been started. e.g.\n\n```typescript\ntype Dependencies = {\n  config: { url: string };\n};\n\nexport function initMongo() {\n  let db;\n\n  async function start({ config }: Dependencies) {\n    db = await MongoClient.connect(config.url);\n    return db;\n  }\n\n  async function stop() {\n    return db.close();\n  }\n\n  return {\n    start,\n    stop,\n  };\n};\n\nconst system = systemic().add('mongo', initMongo());\n```\n\nThe components stop function is useful for when you want to disconnect from an external service or release some other kind of resource.\n\n`@ilpt/systemic-ts` supports multiple types of components:\n\n#### (A)synchronous components\n\n(A)synchronous components look like the `initMongo` component in the example above. They have a start function that returns the underlying resource and an optional stop function. Both start and stop function can be either synchronous or asynchronous.\n\n#### Plain object components\n\nPlain object components do not have a start function and are added to the system as-is. They will not be started or stopped, but can be injected into other component like any other component.\n\n```typescript\nconst logger = {\n  info(message: string) {\n    console.log(message);\n  }\n}\n\nconst system = systemic().add('logger', logger);\n```\n\n#### Function components\n\nFunction components are similar to the `start` function of the (a)synchronous component. The function is called on system start and the returned resource is added to the system.\n\n```typescript\nimport type { BookService } from './book-service';\n\ntype Dependencies = {\n  bookService: BookService\n}\n\nfunction booksDomain({ bookService }: Dependencies) {\n  return {\n    async getBooks() {\n      return bookService.getBooks()\n    }\n  }\n}\n\nconst system = systemic().add('booksDomain', booksDomain)\n```\n\n#### Callback components\n\nSupport for callback components has been dropped in `@ilpt/systemic-ts` in favor of synchronous components. To maintain backwards compatibility with existing components written for legacy `systemic`, `@ilpt/systemic-ts` includes a migration helper to convert them into asynchronous components.\n\n```typescript\nimport initRabbit from 'systemic-rabbitmq';\nimport { promisifyComponent } from '@ilpt/systemic-ts/migrate';\n\nconst system = systemic().add('rabbit', promisifyComponent(initRabbit()));\n```\n\n### Dependencies\n\nA component's dependencies must be registered with the system\n\n```typescript\nimport { systemic } from '@ilpt/systemic-ts';\nimport initConfig from './components/config';\nimport initLogger from './components/logger';\nimport initMongo from './components/mongo';\n\nconst system = systemic()\n  .add('config', initConfig())\n  .add('logger', initLogger()).dependsOn('config')\n  .add('mongo', initMongo()).dependsOn('config', 'logger');\n```\n\nThe components dependencies are injected via it's start function\n\n```typescript\nasync function start({ config }) {\n  db = await MongoClient.connect(config.url);\n  return db;\n}\n```\n\n#### Mapping dependencies\n\nYou can rename dependencies passed to a components start function by specifying a mapping object instead of a simple string\n\n```typescript\nconst system = systemic()\n  .add('config', initConfig())\n  .add('mongo', initMongo())\n  .dependsOn({ component: 'config', destination: 'options' });\n```\n\nIf you want to inject a property or subdocument of the dependency thing you can also express this with a dependency mapping\n\n```typescript\nconst system = systemic()\n  .add('config', initConfig())\n  .add('mongo', initMongo())\n  .dependsOn({ component: 'config', source: 'mongo' });\n```\n\nNow `config.mongo` will be injected as `config` instead of the entire configuration object.\n\n#### Scoped Dependencies\n\nInjecting 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\n\n```typescript\nconst system = systemic()\n  .add('config', initConfig(), { scoped: true })\n  .add('mongo', initMongo())\n  .dependsOn('config');\n```\n\n#### Optional Dependencies\n\nBy 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 that may or may not be available in the system, typically when working with subsystems. In this situation a dependency can be marked as optional.\n\n```typescript\nconst system = systemic()\n  .add('app', app())\n  .add('server', server())\n  .dependsOn('app', { component: 'routes', optional: true });\n```\n\n### Overriding Components\n\nAttempting 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`\n\n```typescript\nimport system from '../src/system';\nimport stub from './stubs/store';\n\nconst testSystem = system().set('store', stub);\n```\n\n### Removing Components\n\nRemoving components during tests can decrease startup time.\n\n```typescript\nimport system from '../src/system';\n\nconst testSystem = system().remove('server');\n```\n\n`@ilpt/systemic-ts` does not allow you to delete components that other components depend on.\n\n### Including components from another system\n\nYou can simplify large systems by breaking them up into smaller ones, then including their component definitions into the main system.\n\n```typescript\n// db-system.ts\nimport { systemic } from '@ilpt/systemic-ts';\nimport initMongo from './components/mongo';\n\ntype DependenciesFromMaster = {\n  logger: Logger;\n  config: { component: Config, scoped: true }\n};\n\nexport function initDbSystem() { \n  return systemic\u003cDependenciesFromMaster\u003e()\n    .add('mongo', initMongo())\n    .dependsOn('config', 'logger');\n};\n```\n\n```typescript\n// system.ts\nimport { systemic } from '@ilpt/systemic-ts';\nimport initUtilSystem from './util-system';\nimport initWebSystem from './web-system';\nimport initDbSystem from './db-system';\n\nimport initConfig from './config';\nimport initLogger from './logger';\n\nconst system = systemic()\n  .add('config', initConfig(), { scoped: true})\n  .add('logger', initLogger()).dependsOn('config')\n  .include(initUtilSystem())\n  .include(initWebSystem())\n  .include(initDbSystem());\n```\n\n### Grouping components\n\nSometimes it's convenient to depend on a group of components. e.g.\n\n```typescript\nconst system = systemic()\n  .add('app', app())\n  .add('routes.admin', adminRoutes())\n  .dependsOn('app')\n  .add('routes.api', apiRoutes())\n  .dependsOn('app')\n  .add('routes')\n  .dependsOn('routes.admin', 'routes.api')\n  .add('server')\n  .dependsOn('app', 'routes');\n```\n\nThe above example will create a component 'routes', which will depend on routes.admin and routes.api and be injected as\n\n```typescript\n {\n  routes: {\n    admin: { ... },\n    adpi: { ... }\n  }\n }\n```\n\n### Debugging\n\nYou 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.\n\n```typescript\n// system.ts\nimport { systemic } from '@ilpt/systemic-ts';\nimport initRoutes from './routes';\n\nconst system = systemic({ name: 'server' })\n  .include(initRoutes());\n```\n\n```typescript\n// routes/index.ts\nimport { systemic } from '@ilpt/systemic-ts';\nimport adminRoutes from './admin-routes';\nimport apiRoutes from './api-routes';\n\nexport default () =\u003e systemic({ name: 'routes' })\n  .add('routes.admin', adminRoutes())\n  .add('routes.api', apiRoutes())\n  .add('routes')\n  .dependsOn('routes.admin', 'routes.api');\n```\n\n```\nDEBUG='systemic:*' node system\nsystemic:index Adding component routes.admin to system routes +0ms\nsystemic:index Adding component routes.api to system auth +2ms\nsystemic:index Adding component routes to system auth +1ms\nsystemic:index Including definitions from sub system routes into system server +0ms\nsystemic:index Starting system server +0ms\nsystemic:index Inspecting component routes.admin +0ms\nsystemic:index Starting component routes.admin +0ms\nsystemic:index Component routes.admin started +15ms\nsystemic:index Inspecting component routes.api +0ms\nsystemic:index Starting component routes.api +0ms\nsystemic:index Component routes.api started +15ms\nsystemic:index Inspecting component routes +0ms\nsystemic:index Injecting dependency routes.admin as routes.admin into routes +0ms\nsystemic:index Injecting dependency routes.api as routes.api into routes +0ms\nsystemic:index Starting component routes +0ms\nsystemic:index Component routes started +15ms\nsystemic:index Injecting dependency routes as routes into server +1ms\nsystemic:index System server started +15ms\n```\n\n## Migration from Systemic to @ilpt/systemic-ts\n\nSince `@ilpt/systemic-ts` is mostly compatible with `systemic`, you can migrate your existing `systemic` service to `@ilpt/systemic-ts` with minimal effort.\n\n### Compatibility\n\n`@ilpt/systemic-ts` is mostly compatible with `systemic`. The differences are:\n- the main `systemic` export is now a named export, for better esm vs commonjs compatibility\n- the `bootstrap` function has been removed, since it was not type safe\n- `@ilpt/systemic-ts` does not support callback components, but includes a migration helper to convert them to asynchronous components\n- the `start` and `stop` functions of the system now return a promise, instead of taking a callback. To maintain compatibility with existing runners, a migration helper is included to convert the system to a callback based system. \n- `systemic` subsystems need to be converted to `@ilpt/systemic-ts` systems with the included migration helper, before they can be included in a `@ilpt/systemic-ts` system.\n\n### Available migration helpers\n\n#### Promisify component\n\nWhen using a callback component, it's best to convert them to an asynchronous component. However, if you're importing a component from a library, you might not be able to change the source code. In this case, you can use the `promisifyComponent` helper to convert the component to an asynchronous component.\n\n```typescript\nimport initRabbit from 'systemic-rabbitmq';\nimport { promisifyComponent } from '@ilpt/systemic-ts/migrate';\n\nconst system = systemic().add('rabbit', promisifyComponent(initRabbit()));\n```\n\n#### Use a legacy runner\n\nIf you're using a runner that expects a callback based system, you can use the `asCallbackSystem` helper to convert the system to a callback based system.\n\n```typescript\nimport { asCallbackSystem } from '@ilpt/systemic-ts/migrate';\nimport runner from 'systemic-service-runner';\n\nimport { initSystem } from './system';\n\nrunner(asCallbackSystem(initSystem())).start((err, components) =\u003e {\n  if (err) throw err;\n  console.log('Started');\n});\n```\n\n#### Upgrade a (sub)system\n\nIf you have a `systemic` subsystem that you want to include in a `@ilpt/systemic-ts` system, you can use the `upgradeSystem` helper to convert the subsystem to a `@ilpt/systemic-ts` system.\n\n```typescript\nimport { upgradeSystem } from '@ilpt/systemic-ts/migrate';\nimport initSubSystem from 'my-legacy-subsystem';\n\nconst system = upgradeSystem(initSubSystem());\n```\n\n### Migration steps\n\n1. Replace all `systemic` imports with `@ilpt/systemic-ts`\n1. Change all callback components to asynchronous components, either by changing the source code or using the `promisifyComponent` helper\n1. If the system includes subsystems that you cannot convert, use the `upgradeSystem` helper to convert them to `@ilpt/systemic-ts` systems.\n1. If subsystems are included using the `bootstrap` functions, use the `include` function instead to add them to the main system. \n1. If you're using a runner that expects a callback based system, choose a different runner or use the `asCallbackSystem` helper to convert the system to a callback based system.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfinitaslearning%2Fsystemic-ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finfinitaslearning%2Fsystemic-ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfinitaslearning%2Fsystemic-ts/lists"}