{"_id":"@coder-mike/poc-infra-generator","_rev":"4-6e8a37ecfa08d78b1a9a556749cbed95","name":"@coder-mike/poc-infra-generator","dist-tags":{"latest":"0.0.5"},"versions":{"0.0.1":{"name":"@coder-mike/poc-infra-generator","description":"A library for streamlined creation and orchestration of distributed systems using Docker","version":"0.0.1","main":"dist/index.js","publishConfig":{"access":"public"},"scripts":{"// build":"Builds the library TypeScript","build":"tsc","// build:watch":"Same as `build`, but watches for changes","build:watch":"tsc -w","// test":"Run mocha tests","test":"echo \"Error: no test specified\" && exit 1","// example:build":"Build the example, including TypeScript and docker","example:build":"npm run build && npm run example:build:gen-infra && npm run example:build:docker","// example:build:gen-infra":"Code-generate the IaC for the example","example:build:gen-infra":"set PERSONA=build-infra && node dist/example.js","// example:build:docker":"Build the docker images for the example","example:build:docker":"docker-compose -f build/docker-compose.yml build","// example:start:docker":"Start the docker containers for the example","example:start:docker":"docker-compose -f build/docker-compose.yml up","// example:start:in-process":"Run the example in-process without needing to build anything","example:start:in-process":"ts-node src/example.ts"},"repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"license":"MIT","bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"homepage":"https://github.com/coder-mike/poc-infra-generator#readme","dependencies":{"@tsconfig/node20":"^1.0.1","@types/node":"^20.3.1","axios":"^1.4.0","express":"^4.18.2","js-yaml":"^4.1.0","readline":"^1.3.0","string-argv":"^0.3.2"},"devDependencies":{"@types/express":"^4.17.17","@types/js-yaml":"^4.0.5","ts-node":"^10.9.1","typescript":"^5.1.3"},"types":"./dist/index.d.ts","gitHead":"cb7db0fef88ccdeb1da4c336a3f2b168d1542a69","_id":"@coder-mike/poc-infra-generator@0.0.1","_nodeVersion":"18.8.0","_npmVersion":"8.18.0","dist":{"integrity":"sha512-iHDedujLjbzCKr4pItvj9rhv+SG3KaKINjVXiI38GM8IqYQUpuiNOLPLdunpE2KbAkYrA34S41YR6kCPJWVbPQ==","shasum":"cd83758f80a5189cf3516c11bd294c13abe5659d","tarball":"https://registry.npmjs.org/@coder-mike/poc-infra-generator/-/poc-infra-generator-0.0.1.tgz","fileCount":66,"unpackedSize":127430,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDcXxLF6nlZ24Dl4TsSbcgg7gJOMZeY4VMR3/pFucBZ4gIhAOpQbATYzAlRtMW7SbylH6+id4vx4ZeH89ybwquNbbzG"}]},"_npmUser":{"name":"coder-mike","email":"elec.mike@gmail.com"},"directories":{},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/poc-infra-generator_0.0.1_1688079591378_0.9479024919067374"},"_hasShrinkwrap":false},"0.0.2":{"name":"@coder-mike/poc-infra-generator","description":"A library for streamlined creation and orchestration of distributed systems using Docker","version":"0.0.2","main":"dist/index.js","publishConfig":{"access":"public"},"scripts":{"// build":"Builds the library TypeScript","build":"tsc","// build:watch":"Same as `build`, but watches for changes","build:watch":"tsc -w","// test":"Run mocha tests","test":"echo \"Error: no test specified\" && exit 1","// example:build":"Build the example, including TypeScript and docker","example:build":"npm run build && npm run example:build:gen-infra && npm run example:build:docker","// example:build:gen-infra":"Code-generate the IaC for the example","example:build:gen-infra":"set PERSONA=build-infra && node dist/example.js","// example:build:docker":"Build the docker images for the example","example:build:docker":"docker-compose -f build/docker-compose.yml build","// example:start:docker":"Start the docker containers for the example","example:start:docker":"docker-compose -f build/docker-compose.yml up --remove-orphans","// example:start:in-process":"Run the example in-process without needing to build anything","example:start:in-process":"ts-node src/example.ts"},"repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"license":"MIT","bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"homepage":"https://github.com/coder-mike/poc-infra-generator#readme","dependencies":{"@tsconfig/node20":"^1.0.1","@types/node":"^20.3.1","axios":"^1.4.0","express":"^4.18.2","js-yaml":"^4.1.0","pg-promise":"^11.5.0","readline":"^1.3.0","string-argv":"^0.3.2"},"devDependencies":{"@types/express":"^4.17.17","@types/js-yaml":"^4.0.5","@types/pg":"^8.10.2","ts-node":"^10.9.1","typescript":"^5.1.3"},"types":"./dist/index.d.ts","gitHead":"a8a99843aea11eb20764fe16e61892309580883e","_id":"@coder-mike/poc-infra-generator@0.0.2","_nodeVersion":"18.8.0","_npmVersion":"8.18.0","dist":{"integrity":"sha512-p9PFJ/D9F5LkGSDmhENjpEOkYChesTQjfQVxgKkm0ir9LP58xwKuHQZnepUuyMrJyRJeitv2tnMXS0hb1AzYjw==","shasum":"6939ea551e631911f9c9d6740c986c762d2c61d9","tarball":"https://registry.npmjs.org/@coder-mike/poc-infra-generator/-/poc-infra-generator-0.0.2.tgz","fileCount":69,"unpackedSize":180517,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIELNVLtyng/3PzuTwRCrmukFHvYx4AEvWGnRfSaMpvNXAiAaYayQzfvh1i57AZoZkDkOt6eEfZKOr/mZBab/nO1t5A=="}]},"_npmUser":{"name":"coder-mike","email":"elec.mike@gmail.com"},"directories":{},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/poc-infra-generator_0.0.2_1688338222289_0.39421535894590787"},"_hasShrinkwrap":false},"0.0.3":{"name":"@coder-mike/poc-infra-generator","description":"A library for streamlined creation and orchestration of distributed systems using Docker","version":"0.0.3","main":"dist/index.js","publishConfig":{"access":"public"},"scripts":{"// build":"Builds the library TypeScript","build":"tsc","// build:watch":"Same as `build`, but watches for changes","build:watch":"tsc -w","// test":"Run mocha tests","test":"echo \"Error: no test specified\" && exit 1","// example:build":"Build the example, including TypeScript and docker","example:build":"npm run build && npm run example:build:gen-infra && npm run example:build:docker","// example:build:gen-infra":"Code-generate the IaC for the example","example:build:gen-infra":"set PERSONA=build-infra && node dist/example.js","// example:build:docker":"Build the docker images for the example","example:build:docker":"docker-compose -f build/docker-compose.yml build","// example:start:docker":"Start the docker containers for the example","example:start:docker":"docker-compose -f build/docker-compose.yml up --remove-orphans","// example:start:in-process":"Run the example in-process without needing to build anything","example:start:in-process":"ts-node src/example.ts"},"repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"license":"MIT","bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"homepage":"https://github.com/coder-mike/poc-infra-generator#readme","dependencies":{"@tsconfig/node20":"^1.0.1","@types/node":"^20.3.1","axios":"^1.4.0","express":"^4.18.2","js-yaml":"^4.1.0","pg-promise":"^11.5.0","readline":"^1.3.0","string-argv":"^0.3.2"},"devDependencies":{"@types/express":"^4.17.17","@types/js-yaml":"^4.0.5","@types/pg":"^8.10.2","ts-node":"^10.9.1","typescript":"^5.1.3"},"types":"./dist/index.d.ts","gitHead":"ffeee7c1eecbf3b406629c2be6624ffe4eec0862","_id":"@coder-mike/poc-infra-generator@0.0.3","_nodeVersion":"18.8.0","_npmVersion":"8.18.0","dist":{"integrity":"sha512-WykBw1hsJ1+ql3l4KEzzKWM2UMgrFnRVI6ivIXYIFkWHhzWo2vY4ASX75RpZUcekR41FIYVSM8MZhIhpqiqTog==","shasum":"c33d738dd8b0b7b617ddf618bbfc24844e280b18","tarball":"https://registry.npmjs.org/@coder-mike/poc-infra-generator/-/poc-infra-generator-0.0.3.tgz","fileCount":72,"unpackedSize":188747,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDlymgph3UCo6F4ndoykI61B+tWYI5WyC66nt7n2iZJuAiA4h+y/T19LqNrQQP8pg4snWg4U2sw70RbfS+W9R79wNA=="}]},"_npmUser":{"name":"coder-mike","email":"elec.mike@gmail.com"},"directories":{},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/poc-infra-generator_0.0.3_1688513342453_0.46512306033559203"},"_hasShrinkwrap":false},"0.0.4":{"name":"@coder-mike/poc-infra-generator","description":"A library for streamlined creation and orchestration of distributed systems using Docker","version":"0.0.4","main":"dist/index.js","publishConfig":{"access":"public"},"scripts":{"// build":"Builds the library TypeScript","build":"tsc","// build:watch":"Same as `build`, but watches for changes","build:watch":"tsc -w","// test":"Run mocha tests","test":"echo \"Error: no test specified\" && exit 1","// example:build":"Build the example, including TypeScript and docker","example:build":"npm run build && npm run example:build:gen-infra && npm run example:build:docker","// example:build:gen-infra":"Code-generate the IaC for the example","example:build:gen-infra":"set PERSONA=build-infra && node dist/example.js","// example:build:docker":"Build the docker images for the example","example:build:docker":"docker-compose -f build/docker-compose.yml build","// example:start:docker":"Start the docker containers for the example","example:start:docker":"docker-compose -f build/docker-compose.yml up --remove-orphans","// example:start:in-process":"Run the example in-process without needing to build anything","example:start:in-process":"ts-node src/example.ts"},"repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"license":"MIT","bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"homepage":"https://github.com/coder-mike/poc-infra-generator#readme","dependencies":{"@tsconfig/node20":"^1.0.1","@types/node":"^20.3.1","axios":"^1.4.0","express":"^4.18.2","js-yaml":"^4.1.0","pg-promise":"^11.5.0","readline":"^1.3.0","string-argv":"^0.3.2"},"devDependencies":{"@types/express":"^4.17.17","@types/js-yaml":"^4.0.5","@types/pg":"^8.10.2","ts-node":"^10.9.1","typescript":"^5.1.3"},"types":"./dist/index.d.ts","gitHead":"9150fd10cfb610e4b84345eb3f8dd44728c62e49","_id":"@coder-mike/poc-infra-generator@0.0.4","_nodeVersion":"18.8.0","_npmVersion":"8.18.0","dist":{"integrity":"sha512-e0llt5NnK35nhHrBR+bYk5d3Yvi8bSh8ijlwAuARmprx/+ZhEwgm69oi7ULoTJkyaQuiWvYwGy7V7AMQP1TVfg==","shasum":"d17435144c878e9ed51ae1af4fb749bb520b03b4","tarball":"https://registry.npmjs.org/@coder-mike/poc-infra-generator/-/poc-infra-generator-0.0.4.tgz","fileCount":63,"unpackedSize":177322,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQD2pQt/L4g6SgBzJU+hWrRMOoGtOVpqf3zZ9PtZ/0sidAIgbMxerYsCzzhVsVL46yKJHPHNOTMQbDGbxqOyj+P53hI="}]},"_npmUser":{"name":"coder-mike","email":"elec.mike@gmail.com"},"directories":{},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/poc-infra-generator_0.0.4_1688516160044_0.01952921884477754"},"_hasShrinkwrap":false},"0.0.5":{"name":"@coder-mike/poc-infra-generator","description":"A library for streamlined creation and orchestration of distributed systems using Docker","version":"0.0.5","main":"dist/index.js","publishConfig":{"access":"public"},"scripts":{"// build":"Builds the library TypeScript","build":"tsc","// build:watch":"Same as `build`, but watches for changes","build:watch":"tsc -w","// test":"Run mocha tests","test":"echo \"Error: no test specified\" && exit 1","// example:build":"Build the example, including TypeScript and docker","example:build":"npm run build && npm run example:build:gen-infra && npm run example:build:docker","// example:build:gen-infra":"Code-generate the IaC for the example","example:build:gen-infra":"set PERSONA=build-infra && node dist/example.js","// example:build:docker":"Build the docker images for the example","example:build:docker":"docker-compose -f build/docker-compose.yml build","// example:start:docker":"Start the docker containers for the example","example:start:docker":"docker-compose -f build/docker-compose.yml up --remove-orphans","// example:start:in-process":"Run the example in-process without needing to build anything","example:start:in-process":"ts-node src/example.ts"},"repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"license":"MIT","bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"homepage":"https://github.com/coder-mike/poc-infra-generator#readme","dependencies":{"@tsconfig/node20":"^1.0.1","@types/node":"^20.3.1","axios":"^1.4.0","express":"^4.18.2","js-yaml":"^4.1.0","pg-promise":"^11.5.0","readline":"^1.3.0","string-argv":"^0.3.2"},"devDependencies":{"@types/express":"^4.17.17","@types/js-yaml":"^4.0.5","@types/pg":"^8.10.2","ts-node":"^10.9.1","typescript":"^5.1.3"},"types":"./dist/index.d.ts","gitHead":"788d7b2cbe17a7d91282b572ff6564bbc4a30753","_id":"@coder-mike/poc-infra-generator@0.0.5","_nodeVersion":"18.8.0","_npmVersion":"8.18.0","dist":{"integrity":"sha512-aU0SEhUhguIZoEbwbsg4bQdi/Hcb5/plMPGdb6MzG1aGK/qZP/TNwNtgifAeT+6SHZZ9ac3efNlkPGlxnkD4mA==","shasum":"99a255e03b1c86e9f907646ba337257ded86b474","tarball":"https://registry.npmjs.org/@coder-mike/poc-infra-generator/-/poc-infra-generator-0.0.5.tgz","fileCount":63,"unpackedSize":179476,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDAMFY2FldgMvVVdGLNvfIgi8BBOWgsYdMFuuzd4aBlxAiB/5wrgL97r/sJ5nVs1zGTs50934dTaygWECq9Kzvd5sg=="}]},"_npmUser":{"name":"coder-mike","email":"elec.mike@gmail.com"},"directories":{},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/poc-infra-generator_0.0.5_1689024622483_0.28983672618987644"},"_hasShrinkwrap":false}},"time":{"created":"2023-06-29T22:59:51.253Z","0.0.1":"2023-06-29T22:59:51.637Z","modified":"2023-07-10T21:30:22.841Z","0.0.2":"2023-07-02T22:50:22.490Z","0.0.3":"2023-07-04T23:29:02.682Z","0.0.4":"2023-07-05T00:16:00.280Z","0.0.5":"2023-07-10T21:30:22.732Z"},"maintainers":[{"name":"coder-mike","email":"elec.mike@gmail.com"}],"description":"A library for streamlined creation and orchestration of distributed systems using Docker","homepage":"https://github.com/coder-mike/poc-infra-generator#readme","repository":{"type":"git","url":"git+https://github.com/coder-mike/poc-infra-generator.git"},"author":{"name":"Michael Hunter"},"bugs":{"url":"https://github.com/coder-mike/poc-infra-generator/issues"},"license":"MIT","readme":"# Proof of concept: Infra Generator\r\n\r\nAuthor: Michael Hunter\r\nLicense: MIT\r\n\r\nInfra Generator is a proof-of-concept library designed to streamline the creation and orchestration of distributed systems. Through its versatile architecture, developers can efficiently encapsulate components, leverage dependency injection, and maintain type-safety. The library provides tools to programmatically define infrastructure configuration, server logic, and client logic, while automatically generating necessary Docker and Docker Compose files to seamlessly deploy multiple architectural components.\r\n\r\n## Disclaimer\r\n\r\nPlease note that this library is a proof-of-concept and is not intended for production use. It is provided as-is, and the author makes no warranties regarding its functionality, completeness, or reliability. Use it at your own risk. The author shall not be responsible for any damages or loss resulting from the use of this library.\r\n\r\n## Prerequisites\r\n\r\nInstall docker and docker-compose.\r\n\r\n## Install\r\n\r\n```sh\r\nnpm install @coder-mike/poc-infra-generator\r\n```\r\n\r\n## Example Usage\r\n\r\nThe following is an example that creates an Express API server called the \"customer-server\" backed by a postgres database containing a table of customers, and an example client application that sends a record to the database through the API and reads it back again.\r\n\r\nThe following is aspirational, since the library is a WIP:\r\n\r\n```ts\r\nimport { rootId, Store, ApiServer, ID, run, Worker } from '@coder-mike/poc-infra-generator';\r\n\r\ninterface Customer {\r\n  id: string;\r\n  name: string;\r\n}\r\n\r\ninterface CustomerServer {\r\n  postCustomer(customer: Customer): Promise<void>;\r\n  getCustomer(id: string): Promise<Customer>;\r\n}\r\n\r\nconst id = rootId('my-app');\r\n\r\n// Create the server (which will create its own database)\r\nconst server = createCustomerServer(id`customer-server`);\r\n\r\n// Create the client, with injected reference to server\r\ncreateExampleClient(id`example-client`, server);\r\n\r\n// Run the current persona\r\nrun();\r\n\r\nfunction createCustomerServer(id: ID): CustomerServer {\r\n  // Create a store for customers (backed by postgres)\r\n  const db = new Store(id`db`);\r\n\r\n  // Create an Express API server\r\n  const server = new ApiServer(id`api`);\r\n\r\n  // Endpoint to post a customer to the database\r\n  const postCustomer = server.defineEndpoint(\r\n    '/api/customer',\r\n    async (customer: Customer) => {\r\n      await db.set(customer.id, customer);\r\n    },\r\n    { method: 'POST' }\r\n  );\r\n\r\n  // Endpoint to get a customer from the database\r\n  const getCustomer = server.defineEndpoint(\r\n    '/api/customer',\r\n    async (id: string) => {\r\n      return db.get(id);\r\n    },\r\n    { method: 'GET' }\r\n  );\r\n\r\n  return {\r\n    postCustomer,\r\n    getCustomer,\r\n  }\r\n}\r\n\r\nfunction createExampleClient(id: ID, server: CustomerServer) {\r\n  // The client will just be a docker container that runs at deployment time\r\n  new Worker(id, async () => {\r\n    // Save customer to the database via the API server\r\n    await server.postCustomer({ id: '1', name: 'John Doe' });\r\n\r\n    // Load customer from the database via the API server\r\n    const customer = await server.getCustomer('1');\r\n\r\n    console.log(`Loaded customer: ${JSON.stringify(customer)}`);\r\n  })\r\n}\r\n```\r\n\r\nTo run this example:\r\n\r\n```sh\r\n# 1. Build the example. This also generates the docker files and docker-compose file\r\nnpm run example:build\r\n\r\n# (2. Note: Please make sure docker desktop is running)\r\n\r\n# 3. Run the whole distributed system (client, server, and database)\r\ndocker-compose -f build/docker-compose.yml up\r\n```\r\n\r\nOr to run the whole example in-process instead of using docker:\r\n\r\n```sh\r\n# Run everything in-process and in-memory. This is useful for debugging.\r\nnpm run example:start:in-process\r\n```\r\n\r\nNote: it's recommended to also include a `.dockerignore` file in your project to prevent the `node_modules` directory from being copied into the docker containers. This will make the docker images smaller and faster to build.\r\n\r\n### Advantages demonstrated in this example\r\n\r\nThere are a few key points to highlight in this example before I explain it:\r\n\r\n- This example is a single script (representative of a single application of many files) but contains code that executes in 3 different places: the client, the server, and build-time configuration of the infra.\r\n\r\n- The `db` in `createCustomerServer` is fully **encapsulated** -- it's a local variable that's not accessible to other parts of the system (e.g. the client). The pattern proposed in this POC makes encapsulation of infra components possible in a way that's a lot harder with traditional infra patterns (e.g. writing Terraform scripts).\r\n\r\n- The `server` in `createExampleClient` is passed in as a parameter. This is an example of **dependency injection** at the infra level.\r\n\r\n- The client's connection to the server here is encapsulated in the function returned from `server.definePost` and `server.defineGet`. These functions handle the details of how to connect to the server and send the HTTP request. In a real-world example, these could also encapsulate authorization and encryption details.\r\n\r\n- The client and server here have a strongly-typed connection between them, without doing any type-casts.\r\n\r\n- Infra components such as `ApiServer`, `Store`, and `onDeploy` have a dual implementation: they can either run in-process or set up the docker infrastructure to run themselves. The ability to run in-process makes debugging easier since you can just breakpoint anywhere in the code and step across component boundaries such as stepping from the client into the calls to the server.\r\n\r\n\r\n### Explanation of Example\r\n\r\n- The example script is run at build time. So functions like `createCustomerServer` and `createExampleClient` are run at build time and in turn run `new Store` and `onDeploy`, which register the relevant pieces that ultimately lead to the generation of the docker and docker-compose files.\r\n\r\n- The example script is copied into each docker container, so it can be run again at runtime in each environment (in this case, the client and server environments).\r\n\r\n- When running in each environment, the script takes on a different *persona*. In the client environment, the script behaves as the client. In the server environment, the script behaves as the server. For example, in the client environment, the on-deploy callback is called, but it the server environment, it is not, even though the callback is instantiated in both.\r\n\r\n- The `id` function is used to generate a deterministic ID for each component -- an ID which is the same for each component at build time and in each runtime environment. This ID is used for all the wiring under the hood, such as the naming of environment variables, docker services, etc. IDs are also functions which can be called using tagged-template syntax to create child IDs, such as the `customer-server` child ID of the `my-app` root ID, and the `db` child ID of the `customer-server` ID. So the full ID of the database is `my-app.customer-server.db`. If the client also wanted a database, it might have it's own `db` ID, but the full ID would be `my-app.example-client.db` to distinguish it from the server.\r\n\r\n\r\n# Concepts\r\n\r\n![](doc\\img\\epochs.png)\r\n\r\n## Startup and Runtime\r\n\r\nExecution in each environment (e.g. server and client) is split into two phases: startup and runtime. The startup phase should behave identically in each environment to instantiate the component tree of the app with exactly the same set of IDs. The startup phase should be deterministic and perform no I/O or random operations, so that the state of the process is identical at the end of each run of the startup phase in each different environment.\r\n\r\nAfter startup, the \"runtime\" phase of execution can begin, where the app behaves differently in each environment (e.g. behaving as a particular client or server).\r\n\r\n## Personas\r\n\r\nThe concept of a persona is a way of describing the environment in which the application is running. For example, the application might be running in a client environment, a server environment, or a build-time environment. The application can behave differently in each environment, but the same code is used in each environment. The running persona is typically determined by the environment variables that are set in the environment. For example, the `PERSONA` environment variable might be set to `my-app.example-client` or `my-app.customer-server.api`. The `PERSONA` environment variable is set by the `docker-compose.yml` file, which in turn is generated by the application script.\r\n\r\nThe library reifies the persona idea in the `Persona` class which is constructed with a callback to be executed at runtime when that persona is active. The `run` function in the library then inspects the `PERSONA` environment variable and determines which `Persona` to run.\r\n\r\n## Build-time execution\r\n\r\nThe build-time persona is a special built-in personal. Like the other personas, it executes the same startup sequence that instantiates the application component tree. But it then specializes its behavior to perform build-time actions:\r\n\r\n- Generating docker and docker-compose files based on the application component tree.\r\n- Reading and writing persistent build-time data stores for things like secrets.\r\n\r\n## Relation to Microvium Snapshotting\r\n\r\nThe concept behind this library is based on the idea behind [Microvium snapshotting](https://github.com/coder-mike/microvium/blob/main/doc/concepts.md). Snapshotting in Microvium runs the application at compile time, and then a snapshot of that application is taken and stored in a binary format. The snapshot is then loaded at runtime and the application is resumed from the snapshot. The same snapshot can be distributed to multiple target environments, such as a server and a client, and they take with them the entire state of the application.\r\n\r\nThrough the snapshotting mechanism, you do not need to manually ensure that the startup phase is deterministic across environments.\r\n\r\nThis library provides a weaker form of the Microvium idea. Rather than deploying a snapshot of the application, the application is re-run in each environment (e.g. client, server, and build-time). The identity of objects is preserved across environments by using the deterministic ID generator. The IDs of each component can be used to identify the same component in other environments, allowing components to behave in a cohesive, distributed manner.\r\n\r\nThis is weaker than the snapshotting paradigm used in Microvium for two reasons:\r\n\r\n1. Microvium snapshots are guaranteed to be identical whereas applications based on this library are relying on the developer to make sure that the application tree is the same each time the startup phase is executed in each environment.\r\n\r\n2. This library relies on always manually defining the IDs to associate components in each environment, whereas in Microvium, all objects have implicit identity, and non-deterministic processes like random number generation can be used to generate unique IDs, which propagate naturally with the snapshot. In this library, if you used a RNG at startup it would just generate different numbers in each environment.\r\n\r\n## Limitations of approach\r\n\r\n- It's manual work to keep the startup sequence the same in every environment, including ID generation.\r\n\r\n- There is no compile-time mechanism to enforce that you call things at the right time. E.g. to stop you from calling `onDeploy` at runtime, or to indicate that you should only use `onDeploy` at build-time. This is mitigated a bit by calling `assertStartup`, `assertRuntime`, etc. at the start of each function, as a check but also as an indication to the reader of the intended location of the execution, but this is not enforced by the compiler.\r\n\r\n## Limitations of POC\r\n\r\n- The POC version of this library uses docker-compose as an IaC foundation, but this is not suitable for production purposes. A future version could use Terraform or some equivalent.\r\n\r\n- There is no resource cleanup process implemented if a new deployment doesn't contain a persistent resource that a previous one did.\r\n\r\n- \"Secrets\" such as port numbers and passwords are currently given to every container, even if the container doesn't need it. A future version could be more selective.\r\n\r\n- Similarly, this POC assumes that everything is accessible to everything on the network, which is not suitable for a production environment. A future version could be more restrictive. This could be as simple as having a `dependsOn` clause in each service to declare the injected dependencies, and then this can be used to auto-generate the network restrictions and environment variables.\r\n\r\n- Indexer functions in the Store are assumed to be immutable. If you change the implementation of an indexer function, you need to change the ID of the indexer (e.g. appending a version number to the ID).\r\n\r\n- Because indexes are not cleaned up, if you have orphaned indexes in the database, they will cause foreign key constraint errors when you try to delete data that's referenced by those indexes. You need to manually clean up the index tables (e.g. delete them with pgAdmin).\r\n\r\n- Postgres instances launched with docker-compose have their password configuration embedded into the volume after the first use, so if you change the password (or you delete the `passwords.json` file in the build output), you need to manually delete the volume.\r\n\r\n# Component Reference (High-level components)\r\n\r\n## `Store`\r\n\r\nA key-value store for JSON values, with support for indexing. The store is implemented as a Postgres database when running on docker-compose and implemented as an in-memory store when running in-process.\r\n\r\nThe store supports the following operations:\r\n\r\n- `store.get`: Get a value by key.\r\n- `store.set`: Set a value by key.\r\n- `store.has`: Check if a key exists.\r\n- `store.del`: Delete a value by key.\r\n- `store.modify`: Atomic read-modify-write operation.\r\n- `store.allKeys`: Get all keys in the store.\r\n- `new store.Index`: Define a new index on the store.\r\n\r\n### Indexes\r\n\r\nExample:\r\n\r\n```ts\r\ninterface Message {\r\n  text: string;\r\n  from: string;\r\n  to: string;\r\n}\r\n\r\n// At startup\r\nassertStartupTime();\r\nconst store = new Store<Message>(id`store`)\r\n// Let's say we want to index by the `from` field\r\nconst index = new store.Index(id`index`, (value) => [{\r\n  indexKey: value.from,\r\n  // Inline the `to` value in the index for quick access (optional)\r\n  inlineValue: value.to,\r\n}]);\r\n\r\n// ...\r\n\r\n// Later, at runtime\r\nassertRuntime();\r\n// Get all messages from Alice\r\nconst fromAlice = await fromIndex.get('Alice');\r\nconsole.log(fromAlice.map(m => `message from ${m.indexKey} to ${m.inlineValue}`));\r\n```\r\n\r\nUnder the hood, indexes work by creating a separate table in the database for each index and keeping it in sync with the main table.\r\n\r\n\r\n## `CliCommand`\r\n\r\nRegister a new CLI command with `new CliCommand(id, name, entrypointCallback)`.\r\n\r\nWhen running in-process, the application will enter an interactive REPL loop where it accepts commands from the user and dispatches them to the corresponding `entrypointCallback` by name.\r\n\r\nWhen running in docker-compose, all the CliCommands are generated as independent shell scripts with access to the shared secrets (passwords, host names, and port numbers) necessary to connect to other components of the system (e.g. stores and API servers).\r\n\r\nYou can invoke a `CliCommand` with `command.run()`. If running in-process, this will parse the arguments and call the callback directly. If running in docker-compose, this will execute the generated shell script.\r\n\r\n\r\n## `ApiServer`\r\n\r\nAn HTTP server to expose an API.\r\n\r\nCall `apiServer.defineEndpoint` to register a new handler for a particular endpoint route. You can optionally specify which HTTP method to use (e.g. `GET` or `POST`).\r\n\r\nThe return value from `defineEndpoint` is a function that you can call to make a request to the endpoint from a client. If running in-process, this will call the handler directly. If running in docker-compose, this will make an HTTP request to the generated API server using axios.\r\n\r\nIf running in-process, there is no actual HTTP server. Instead, the API server is implemented as a set of functions that you can call directly. This is useful for testing and debugging. If running in docker-compose, the API server is implemented as an express.js HTTP server in its own docker container.\r\n\r\n\r\n## `Worker`\r\n\r\n`const worker = new Worker(id, callback, opts)`\r\n\r\nA worker represents a long-running background process. If the system is in in-process mode, the worker is executed as a direct call to the callback at startup. If the system is in docker-compose mode, the worker is instantiated as a separate docker container and the callback is called when the docker container starts.\r\n\r\nFor example, the `Worker` component is the basis of the `ApiServer` component, where the `callback` starts an express.js server.\r\n\r\n\r\n# Epoch Assertions\r\n\r\nAll personas share the same startup epoch, which should run deterministically to establish the tree of components.\r\n\r\n![](doc\\img\\epochs.png)\r\n\r\nUse one of the following assertions at the beginning of each function that you want to run in a particular epoch. The assertion serves as a check but also as a statement of intention to the reader.\r\n\r\n- `assertStartupTime` - In the startup phase of any persona (including the build-time persona).\r\n- `assertNotStartup` - Opposite of `assertStartupTime`.\r\n- `assertRuntime` - Running in a non-build persona and not in the startup phase.\r\n- `assertBuildTime` - Running in the build-time persona, either at startup or not.\r\n\r\n# Low Level Components\r\n\r\n- `onBuild` Register a callback to be executed at build time, such as to generated a file.\r\n- `BuildTimeFile` A file that is only written at build time, to keep information persistent across multiple builds.\r\n- `BuildTimeStore` A simple build-time key-value store built on `BuildTimeFile`.\r\n- `gitIgnorePath` Add a path to the `.gitignore` file in the `build` directory.\r\n- `DockerService` Add a docker service to the `docker-compose.yml` file.\r\n- `DockerVolume` Add a docker volume to the `docker-compose.yml` file.\r\n- `DockerFile` Add a docker file to the `build` directory.\r\n- `Secret` Add a piece of configuration information to pass from build time to runtime. The `DockerService` and `CliCommand` components have built-in knowledge of the secrets and so can integrate them into the docker-compose file or `.env` file respectively so that they can be rehydrated at runtime.\r\n- `Password` Create a `Secret` that's randomly generated once and then persisted in the `passwords.json` file.\r\n- `Persona` Define a persona that can be run at runtime.\r\n- `Port` Allocate a new port number. This is persisted in the `ports.json` compile-time file so that it's consistent across builds. The port numbers start at `350000`.\r\n","readmeFilename":"readme.md"}