{"_id":"@douglasgreyling/light-service","_rev":"7-cee1ba8fd860ba0202941a72f21f3ee9","name":"@douglasgreyling/light-service","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@douglasgreyling/light-service","version":"1.0.0","keywords":["service","services","service-objects","lightservice","light-service"],"author":"","license":"MIT","_id":"@douglasgreyling/light-service@1.0.0","maintainers":[{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"}],"homepage":"https://github.com/douglasgreyling/light-service.js#readme","bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"dist":{"shasum":"2df51debae70a7e286c3d35f09d255180cbf3cd1","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-1.0.0.tgz","fileCount":48,"integrity":"sha512-8fTrqMEZhmahgT4xr+3U9XsQO35NBcL8jhjpz2nh1iamBIL2knyWt5zFYVB4IiRYbS+q5S6zZQ7N+VkiK92zcg==","signatures":[{"sig":"MEQCIEUwsBSozHPjGS6qTxQcxDhXnWk+xPHKFEJitrFmZoB+AiAERQu7588g4X5UG+ZfiIL6FJ4hijePc/1jVqDMZwPxbA==","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":133339,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhNm6xCRA9TVsSAnZWagAAz3sP/2SsZ085F0FbuZKg3xY4\nMZGBZAoS545E4Cahvv1i6RGkI2PoeUXgSwAWGbOEWOyngkEow+xQt4NGLubp\nKZieuBy+YzSlNX6FbdkB9VdKMTiRZgcGXsrNJr3dZiydKZoiII+DQfyJ62d4\nupwApQ38UR6TpO5sQ2LKggYV/TA4lI8tvzFTJCXxJVe+sK1LBPGAkXWz5283\nvQ6YXSaQWKPfGSztbVoIZfHx62Ii8hEYnNDLhTEGgHHHCbT1QUBc+IPeqZDM\npxb5NhYt57/lLy6k2Lz5fLJLqNodVCS6GvKi3lBOEzx+8+QC6wYg7kZTANuo\nygifrhJgvUTZ/hnbgO4ACy8aLfnAvcaT4bLnPulxE6XIib8xEa95OIXxoM7q\nLV6d32p0b3hQNe7ipzVNrOMoDcVt4axV3XS9OiVgZAZmeG+AP9ZeItx1vz/c\nuWCPzObYx1cbLzx71rsXE/qHvQZ0ZYMRWcDXf7WpMVdKT2mzor+EShLfhWV+\neAQbeaIq88t69G5D4QGu61cNgw+j6ZTKR1n2OK9tHKWRqYMScrWeUwnDgzC3\nKD2y1KzC7bjOoA/Dp5JjrGzdHQeOJ7A9I4v7BbXbiqSJQVLWG2X4jTatWh4Y\nijWVx+MTJfIYcufsvsRGDrRL6eNwyY8d1Y2Adxkg9S9IZvsMsHhGtS9Mhhvq\nHE/6\r\n=dyYK\r\n-----END PGP SIGNATURE-----\r\n"},"main":"index.js","type":"module","gitHead":"932216ac342e99cc405a67e710ff732bab7e2a51","scripts":{"test":"cross-env NODE_OPTIONS=--experimental-vm-modules jest"},"_npmUser":{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"},"repository":{"url":"git+https://github.com/douglasgreyling/light-service.js.git","type":"git"},"_npmVersion":"7.22.0","description":"A service object framework for JavaScript","directories":{"test":"tests"},"_nodeVersion":"14.16.0","dependencies":{"p-pipe":"^4.0.0","p-each-series":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^27.1.0","cross-env":"^7.0.3"},"_npmOperationalInternal":{"tmp":"tmp/light-service_1.0.0_1630957233443_0.9544418637862904","host":"s3://npm-registry-packages"}},"1.0.1":{"name":"@douglasgreyling/light-service","version":"1.0.1","keywords":["service","services","service-objects","lightservice","light-service"],"author":"","license":"MIT","_id":"@douglasgreyling/light-service@1.0.1","maintainers":[{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"}],"homepage":"https://github.com/douglasgreyling/light-service.js#readme","bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"dist":{"shasum":"9f9770f15b344bbe208e9ac4cf39f93d2c4cb778","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-1.0.1.tgz","fileCount":48,"integrity":"sha512-FNVjiQBiQYxUSbBDU7FvdgvLWzfcD7YF3CqxSMuqTtrHcxCJwIwmI3xtfCe904D4YOj+HgoE/EuIUn0UO2WBxg==","signatures":[{"sig":"MEYCIQCIblZ3npltTI+RVh5t0kctugwQpDRS0VgFo03ytdIa3wIhAPwWF2zjTLUohTVdqLvr7plADs92zDHC1wdiVfBgkGOF","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":133225,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhNm+8CRA9TVsSAnZWagAAnIAP/2r8Er57woUSpI7ip3uH\nTfVmMxaRp+yE2otetOR9LhXPejlcVBEwzWtQ7g0XbID8hgkGArn8g6+BzKJR\n8GcPWtWOomNIJFZfMjbjDOEBAe5TFVB++SZpUysQMgO0RHruyNMNv/ZXZ/iE\nfotF8ZiNj2dnPGz//cyWoZfRnodjSfvf/1Hvg35gPUnDri7nKHR0ehmm5QOS\nJCBCUjtE45EXFbWrp2eLYE93ecYG4mx5xxkhXCQEe5yCbLaycpMtwtKbhQ/t\nsrMlIddOxrL5Mmf3tkOtj+XVOV6dhpYVU86gbqE5Ml/6jk70e/KGYf7Rk3vH\nPiV2IG0O+Rf8Upgh0pp1eaAyG6W6o70m9u1YDEd2uNXKo6ODPPEi/b/SZ8m+\n+3ON41U8JtkbrhpZ7cYSJ6Oz1dgvAnQ8lq3fac3X6VavaM+89RLUEuzvvW6E\nWfYe1T4s/eGVyVOM/8XhaHS+lBb7GA5bs9zMvmgJ30XxUxCiDCr+IwyCnz88\nvXea+IxYXvxiDmzfVcFjMQTEgR1zxeoysAM90zF2nJ78gmd5m4IkSdw6FRtE\nOVVDe1TJl/hCsCXrooOH04BFApguf/ZkDorPx+/Ea3Nidyc/6FJ/F4Z4F5nM\nNcQOPghuAmW/9UyHvWKEKLqlCXvLx08I1Yx2rzPtEoBGnHV5GQI5C8VTjZ4a\nLoKR\r\n=pyH3\r\n-----END PGP SIGNATURE-----\r\n"},"main":"index.js","type":"module","gitHead":"76ac49786776a0dae1c316803a4541411a38f9a8","scripts":{"test":"cross-env NODE_OPTIONS=--experimental-vm-modules jest"},"_npmUser":{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"},"repository":{"url":"git+https://github.com/douglasgreyling/light-service.js.git","type":"git"},"_npmVersion":"7.22.0","description":"A service object framework for JavaScript","directories":{"test":"tests"},"_nodeVersion":"14.16.0","dependencies":{"p-pipe":"^4.0.0","p-each-series":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^27.1.0","cross-env":"^7.0.3"},"_npmOperationalInternal":{"tmp":"tmp/light-service_1.0.1_1630957499889_0.4555495610539402","host":"s3://npm-registry-packages"}},"1.0.2":{"name":"@douglasgreyling/light-service","version":"1.0.2","keywords":["service","services","service-objects","lightservice","light-service"],"author":"","license":"MIT","_id":"@douglasgreyling/light-service@1.0.2","maintainers":[{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"}],"homepage":"https://github.com/douglasgreyling/light-service.js#readme","bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"dist":{"shasum":"c5f9508bef677ef686028c11449e85a1a19c0bd5","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-1.0.2.tgz","fileCount":48,"integrity":"sha512-Ozk7OPjibGlwMKSWwDEIFS8n9yb7fH4eEpQtg7hLClVbVm8Gge49FVLdRzmTKUhaa070KOI46V9Om5sTudgPmA==","signatures":[{"sig":"MEUCIQDd8VOFbY544NojywptH9jNWaqS3/wOkFNqmlCFJ8y0uwIgRsi1L8v0Y3ULKncxmCXbIyOgcoO1SLBo4ZEve251Di4=","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":133233,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhNnBKCRA9TVsSAnZWagAAs78P/2KLsLjzgixqtL8NSoTX\noacGzWH5BEDj5+xAjKf0jVPlLbKJRFademxREIP7py5ISCBvislF55Bf19k1\nWtoknSkdjJXF/35lgveNIIAjsubg1aveOg39nNiYQS0htuIHJBLtr68zIUD2\n3l4HsVOqRQuBIXHkDSHTCKeIxPeFKPTXqkSoR5eR269cU/pQAf3JJLO5sKpb\ntlBKuDUJIrTd3HEbTuthIKNGhGWIEjqGxC4DIhU6xsYV0eNTE6Hm07e0bafV\nkTQWKbWbfs1Qx6BPp68FCFpL3dUR/mQkIsUAxre7gnXijDZ/C6abA+xvvwPD\nulZHEHYuw9/q8swr6XQnp4KZyvYvxniFVBNbMlftx9+X0+tazfeXuVbsbCwp\nKPViVvI37xPUqLWjtIEJsX3UsaFd/qB1BxjT9wWgyO/KgjbzShPBKq5gtwSz\n8zOzi+8IZu3KcqVXG3DG7ogOzl6BRP4E3WDsE5uHRFxREzchDPkKjPUf9Ren\nX54+k645U/JTAPXPGYwJninNxFZokjDVqAqf/UN4MDhztP4Y6JtCXrr3dUOR\nF9HbfpQeg5LAWv3GLrk1S4JzB399gkkEDXGMWDPm+PsS4V8otWIB57wihQPy\n24AxYg12gguhPpMCL8Qhvl96jJCp79nSawkB0yL0F2AxK3+sOdLTeWrj9czd\n4Mzf\r\n=9AmH\r\n-----END PGP SIGNATURE-----\r\n"},"main":"index.js","type":"module","gitHead":"eb974c97f3494c329c076fd40c957543eb2aef0e","scripts":{"test":"cross-env NODE_OPTIONS=--experimental-vm-modules jest"},"_npmUser":{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"},"repository":{"url":"git+https://github.com/douglasgreyling/light-service.js.git","type":"git"},"_npmVersion":"7.22.0","description":"A service object framework for JavaScript","directories":{"test":"tests"},"_nodeVersion":"14.16.0","dependencies":{"p-pipe":"^4.0.0","p-each-series":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^27.1.0","cross-env":"^7.0.3"},"_npmOperationalInternal":{"tmp":"tmp/light-service_1.0.2_1630957641578_0.01805654596601447","host":"s3://npm-registry-packages"}},"1.0.3":{"name":"@douglasgreyling/light-service","version":"1.0.3","keywords":["service","services","service-objects","lightservice","light-service"],"author":"","license":"MIT","_id":"@douglasgreyling/light-service@1.0.3","maintainers":[{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"}],"homepage":"https://github.com/douglasgreyling/light-service.js#readme","bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"dist":{"shasum":"6b22fd38f504e441b99a3af795905d7664d16477","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-1.0.3.tgz","fileCount":48,"integrity":"sha512-xrOkqaNk/9/TXMs0MydPtxLJCzdI1F8QnQlhaKhyBZv5D0RW+K7P9rDqrYkuKXO3ulsMY5t/EsNaskGqbJLFLA==","signatures":[{"sig":"MEUCIBGD562JysNTyRaGMMrbhMNT13dP6niXtLlhZgVqLWwTAiEA31+bWrZRNwbfAQNbTiwWuI5bQk6odQV0vd+wzIUf/gU=","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":133232,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhNnGVCRA9TVsSAnZWagAA1rsP/iQLknb4AQKlZP9k2Wvu\nt3p/PNLGwby41OXLfxcjRHs2VRx3nVofnFQr2JkcZ2WNXZbbeKJn9R/tf110\nqWb1T0L1DZqLMYG42r5y7w3CepTzaLWxLhlAJ9GV+2O9sJgrZ4JLO9VyZHq0\nACCyc3ByVjXsTSHX5ImIa1dPP9U9vvbDqZGN4dJRfQb0yLoiV7NLCLFPcBHL\nZJCLNE1nPHF7CFUwXqDdOk2N7EyD+jX/EoCFgQOhc/9LRSaz/VsRc68Q552D\n0NZfevDjdua9Q69uwzSjqAb9L942ldtx2lcHmP7jpk8lpumPYXKMCPiioa+Z\nf+Xsj2ntHAsOuFPt7eI+L4cVmQd+/wf+e9Ie2HrzGlvjokTS0V5I2oFORXYu\n4U/V2D0mwS6eWYuHJJ+2fTadDmFdUtAiWWOxphVSnMRSvlhQMz8wtVae7b5W\nGkQfHjKSy0bykfMbIeGoWxQKRGg1Ur5cMdAprKgDSlufHoT0g3SQgZCOnbBu\npDYi7z0zpEnlIWhy3PBHjzZ/+OLT//8AQUXEjwiwT8QwU03cGbdyRVGnf9Bc\newL32avHPNFvYFYsIELHtKgadNYwphY7bbXxrQ2rnVZe+dQ+vVZiVhfZHnJ7\nnZ8L5nznqh778getZfp8ylsmMO22P7NeJEsGr7lg6cAW4RxPDlpOUWwYe+hx\nB3pD\r\n=B2+G\r\n-----END PGP SIGNATURE-----\r\n"},"main":"index.js","type":"module","gitHead":"fe89b93afc577a14d2ca93e632ea109c315d60ef","scripts":{"test":"cross-env NODE_OPTIONS=--experimental-vm-modules jest"},"_npmUser":{"name":"douglasgreyling","email":"greyling.douglas@gmail.com"},"repository":{"url":"git+https://github.com/douglasgreyling/light-service.js.git","type":"git"},"_npmVersion":"7.22.0","description":"A service object framework for JavaScript","directories":{"test":"tests"},"_nodeVersion":"14.16.0","dependencies":{"p-pipe":"^4.0.0","p-each-series":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^27.1.0","cross-env":"^7.0.3"},"_npmOperationalInternal":{"tmp":"tmp/light-service_1.0.3_1630957972908_0.49567669703920614","host":"s3://npm-registry-packages"}},"1.1.3":{"name":"@douglasgreyling/light-service","version":"1.1.3","keywords":["service","services","service-objects","lightservice","light-service"],"author":"","license":"MIT","_id":"@douglasgreyling/light-service@1.1.3","maintainers":[{"name":"douglasgreyling","email":"doug@siliconvaalies.co.za"}],"homepage":"https://github.com/douglasgreyling/light-service.js#readme","bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"dist":{"shasum":"3cf7c1e2d339e80b951842c809ed73924597bafe","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-1.1.3.tgz","fileCount":49,"integrity":"sha512-5kSp4iRAlSN7NHnkOnVlaPyltcCDNj/m6ZzT20dVrjuT709mGmjFUTzYQiicvutnRolyXlryVEwSPI4EIUeJqg==","signatures":[{"sig":"MEYCIQCBGaoPyzB+vOh99MfisOlRhIyI1nEeVtcCpUkZUOyUXgIhAIv29loeVFmcQtUcR9eUr64S6H0uviJUEIDkn3JC4n8j","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":134276,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhOM7SCRA9TVsSAnZWagAA1pEP/joYoyIsC9DngOSa94xE\nHk6xcQcrIDtUMPzGv0N4f4FCeqyw/jGQJn59mq407PUUVkfdJVXRiSbGDKzR\nJ0jZ9WyFN6vuOxKQKItRGowcjJZgY1cgwD4Gas9u+5Gd+7Kyj9ysX5rYgiID\n/KrwlHcADnaSRq6o6/SE6Kpp+JVGL5QRiRPxYwZ8p6J3Xm8xRE1oonuR19Gq\nv5wygBEA+PBm71YqK5ir5QCiDTVf27Pba8qJRQQbmaeEeeJ5/DAV4vrTMhmY\nxOX84w6451Mxnls2u17RxAFUun9qL6wfDpn9WWKknQSTlvGOgtqHo3zD2ft2\nfV0dzFIajsr5yqnrWv7+KAtyZ6g6bafiwglf5LnLeQ8Wmt8BO+bSrVDd3fHJ\n5Ge4BGOjbfjzPDIwfmpjW+V/AagH7BlxlmBSf1TuPcao8o6XJH+viaX7+p4n\nKrXNZToUGNE7P3JcE+gtTcf+HwJOe+M8kx39m+n06+bWcRngv4XLuq7DxJ5S\ny/sezyRWp/+CRG7hoU4jKuQP57UKg+Jz5XxQ5v0m8B4CQq4zyEvSxWRu3koK\nUKZc4bAhKf5HIGc2ibRTZh5pNZah87klfNlJ6pmr025rLH8M5RrBwJS0JQnj\nvrhIkgkZ2wtxV+NlbZ8lJizarmCafDkb50MX5FRcRQdcgOLFUDl8KNKZEMvb\nNJzE\r\n=56H7\r\n-----END PGP SIGNATURE-----\r\n"},"main":"index.js","gitHead":"99c7f314dcbc25fd52ea5b5bc70f2e25df58a6e8","scripts":{"test":"jest --coverage"},"_npmUser":{"name":"douglasgreyling","email":"doug@siliconvaalies.co.za"},"repository":{"url":"git+https://github.com/douglasgreyling/light-service.js.git","type":"git"},"_npmVersion":"7.22.0","description":"A service object framework for JavaScript","directories":{"test":"tests"},"_nodeVersion":"14.16.0","dependencies":{"p-pipe":"^3.1.0","p-each-series":"^2.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^27.1.0","cross-env":"^7.0.3"},"_npmOperationalInternal":{"tmp":"tmp/light-service_1.1.3_1631112914437_0.9807028219046634","host":"s3://npm-registry-packages"}},"2.0.0":{"name":"@douglasgreyling/light-service","version":"2.0.0","description":"A service object framework for JavaScript","main":"index.js","directories":{"test":"tests"},"devDependencies":{"@eslint/js":"^10.0.1","eslint":"^10.9.1","eslint-config-prettier":"^10.1.8","eslint-plugin-jest":"^29.16.1","globals":"^17.11.0","jest":"^30.4.2","prettier":"^3.9.6"},"scripts":{"test":"jest --coverage","test:watch":"jest --watchAll","lint":"eslint .","lint:fix":"eslint . --fix","format":"prettier --write .","format:check":"prettier --check ."},"author":"","license":"MIT","repository":{"type":"git","url":"git+https://github.com/douglasgreyling/light-service.js.git"},"bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"homepage":"https://github.com/douglasgreyling/light-service.js#readme","keywords":["service","services","service-objects","lightservice","light-service"],"allowScripts":{"unrs-resolver":false},"gitHead":"d05b930c04bb7350967edb1f49b231874ac5ed89","_id":"@douglasgreyling/light-service@2.0.0","_nodeVersion":"24.11.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-tay5HoQa6J3bLWvElYMEzMl1YH9t/e/U00x1jGGerLEcq3jT+IcvP5PZk4QrtUfjaajhzU7sn10M0aYswAz19A==","shasum":"43a7ba3d3a0b8457f363d7dad18d501ec84b265d","tarball":"https://registry.npmjs.org/@douglasgreyling/light-service/-/light-service-2.0.0.tgz","fileCount":15,"unpackedSize":40836,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICWdFoZQ6IGcS20xE1y7eFSawX88xK7C9sLQ2atojD+7AiBd7ZnAdVDZCY+hAhHHnD/qVpXChEwI7/IpiNHoiZrZFQ=="}]},"_npmUser":{"name":"douglasgreyling","email":"doug@siliconvaalies.co.za"},"maintainers":[{"name":"douglasgreyling","email":"doug@siliconvaalies.co.za"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/light-service_2.0.0_1787660504950_0.979736508017583"},"_hasShrinkwrap":false}},"time":{"created":"2021-09-06T19:40:33.393Z","modified":"2026-08-25T12:21:45.315Z","1.0.0":"2021-09-06T19:40:33.586Z","1.0.1":"2021-09-06T19:45:00.045Z","1.0.2":"2021-09-06T19:47:22.058Z","1.0.3":"2021-09-06T19:52:53.068Z","1.1.3":"2021-09-08T14:55:14.615Z","2.0.0":"2026-08-25T12:21:45.100Z"},"bugs":{"url":"https://github.com/douglasgreyling/light-service.js/issues"},"license":"MIT","homepage":"https://github.com/douglasgreyling/light-service.js#readme","keywords":["service","services","service-objects","lightservice","light-service"],"repository":{"type":"git","url":"git+https://github.com/douglasgreyling/light-service.js.git"},"description":"A service object framework for JavaScript","maintainers":[{"name":"douglasgreyling","email":"doug@siliconvaalies.co.za"}],"readme":"# LightService\n\n![NPM Version](https://img.shields.io/npm/v/@douglasgreyling/light-service)\n[![build Actions Status](https://github.com/douglasgreyling/light-service.js/workflows/build/badge.svg)](https://github.com/douglasgreyling/light-service.js/actions)\n[![codecov](https://codecov.io/gh/douglasgreyling/light-service.js/branch/main/graph/badge.svg?token=LN2G57L77T)](https://codecov.io/gh/douglasgreyling/light-service.js)\n![NPM License](https://img.shields.io/npm/l/@douglasgreyling/light-service)\n\nA service object framework heavily, heavily, heavily inspired by the [LightService](https://github.com/adomokos/light-service) Ruby gem.\n\nThis package ports over most of the awesome ideas in [LightService](https://github.com/adomokos/light-service) so that one can use it in JavaScript. If you're familiar with the Ruby version, then you should feel mostly at home with this package.\n\nBe sure to check out the original [LightService](https://github.com/adomokos/light-service) if you ever find yourself in Ruby-land!\n\n## Table of Content\n\n- [Why LightService?](#why-lightservice)\n- [How LightService works in 60 seconds](#how-lightservice-works-in-60-seconds)\n- [Getting started](#getting-started)\n  - [Installation](#installation)\n  - [Your first action](#your-first-action)\n  - [Your first organizer](#your-first-organizer)\n- [Simplifying our first tax example](#simplifying-our-first-tax-example)\n  - [The organizer](#the-organizer)\n  - [Looking up the tax percentage](#looking-up-the-tax-percentage)\n  - [Calculating the order tax](#calculating-the-order-tax)\n  - [Providing free shipping (where applicable)](#providing-free-shipping-where-applicable)\n  - [And finally, the controller](#and-finally-the-controller)\n- [Caveats](#caveats)\n- [Tips & Tricks](#tips--tricks)\n  - [Stopping a series of actions](#stopping-a-series-of-actions)\n  - [Hooks](#hooks)\n  - [Expects and promises](#expects-and-promises)\n  - [Context](#context)\n  - [Key aliases](#key-aliases)\n  - [Error codes](#error-codes)\n  - [Action rollback](#action-rollback)\n- [Development](#development)\n  - [Linting and formatting](#linting-and-formatting)\n  - [Auditing dependencies](#auditing-dependencies)\n  - [Working without Docker](#working-without-docker)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Why LightService?\n\nWhat do you think of this code?\n\n```javascript\nclass TaxController extends SomeController {\n  update(request, response) {\n    const order = Order.find(request.id);\n    const taxRanges = TaxRange.forRegion(order.region);\n\n    if (taxRanges === undefined)\n      return ...; // render some view\n\n    const taxPercentage = taxRanges.forTotal(order.total);\n\n    if (taxRanges === undefined)\n      return ...; // render some other view\n\n    order.tax = (\n      context.order.total *\n      (context.taxPercentage / 100)\n    ).toFixed(2);\n\n    if (order.totalWithTax > 200)\n      order.provideFreeShipping();\n\n    return ...; // Redirect to some view with a flash message\n  }\n}\n```\n\nThis controller violates the [SRP](http://en.wikipedia.org/wiki/Single_responsibility_principle). Can you imagine testing something like this?\n\nIn this instance we have a fairly simple controller, but one shudders to think what controllers could look like in more complex codebases out there in the wild.\n\nYou could argue that you could clean up this controller by moving the `taxPercentage` logic and calculations into a tax model, but then you'll be relying on heavy model logic.\n\nIf you've ever done debugging (haha, who hasn't?) you might find it difficult to determine what's going on and where you need to start. This is especially difficult when you have a high level overview of what the code does and what needs to happen to resolve your bug.\n\nWouldn't it be nice if your code was broken up into smaller pieces which tell you exactly what they do?\n\nIn the case with our controller above, it would be great if our code dispelled any confusion by telling us that it was doing 3 simple things in a specific sequence whenever an order is updated:\n\n1. Looking up the tax percentage based on order total.\n2. Calculating the order tax.\n3. Providing free shipping if the total with tax is greater than \\$200.\n\nIf you've ever felt the headache of fat controllers, difficult code to reason about, or seemingly endless rabbit holes, then this is where LightService comes in.\n\n## How LightService works in 60 seconds\n\nThere are 2 key things to know about when working with LightService:\n\n1. Actions.\n2. Organizers.\n\n**Actions** are the building blocks of getting stuff done in LightService. Actions focus on doing one thing really well. They can be executed on their own, but you'll often see them bundled together with other actions inside Organizers.\n\n**Organizers** group multiple actions together to complete some task. Organizers consist of at least one action. Organizers execute actions in a set order, one at a time. Organizers use actions to tell you the 'story' of what will happen.\n\nHere's a diagram to understand the relationship between organizers and actions:\n\n![LightService](resources/lightservice-interaction.png)\n\n## Getting started\n\n### Installation\n\n```\nnpm i @douglasgreyling/light-service\n```\n\nThe package exports the two classes you build with, plus the `Context` class and\nthe errors it throws, so you can identify them with `instanceof`:\n\n```javascript\nconst {\n  Action,\n  Organizer,\n  Context,\n  ExpectedKeysNotInContextError,\n  PromisedKeysNotInContextError,\n  RollbackError,\n  SkipActionError,\n  AliasKeyAlreadyInContextError,\n  ReservedContextKeysError,\n} = require(\"@douglasgreyling/light-service\");\n```\n\n### Your first action\n\nLet's make a simple greeting action.\n\n```javascript\nimport { Action } from \"@douglasgreyling/light-service\";\n\nclass GreetsSomeoneAction extends Action {\n  expects = [\"name\"];\n  promises = [\"greeting\"];\n\n  executed(context) {\n    context.greeting = `Hello, ${context.name}. Solved any fun mysteries lately?`;\n  }\n}\n```\n\nActions take an optional list of expected inputs and can return an optional list of promised outputs. In this case we've told our action that it expects to receive an input called `name`.\n\nThe `executed` function is the function which gets called whenever we execute/run our action. We can access the inputs available to this action through the `context` argument. Likewise, we can add/set any outputs through the context as well.\n\nOnce an action is run we can access the finished context and the status of the action.\n\n```javascript\nconst result = await GreetsSomeoneAction.execute({ name: \"Scooby\" });\n\nif (result.success()) console.log(result.greeting);\n\n// => \"Hello, Scooby. Solved any fun mysteries lately?\"\n```\n\nActions try to promote simplicity. They either succeed, or they fail, and they have very clear inputs and outputs. They generally focus on doing one thing, and because of that they can be a dream to test!\n\n### Your first organizer\n\nMost times a simple action isn't enough. LightService lets you compose a bunch of actions into a single organizer. By bundling your simple actions into an organizer you can stitch very complicated business logic together in a manner that's very easy to reason about. Good organizers tell you a clear story!\n\nBefore we create our organizer, let's create one more action:\n\n```javascript\nclass FeedsSomeoneAction extends Action {\n  expects = [\"name\"];\n\n  executed(context) {\n    const snack = Fridge.fetch(\"Grapes\");\n\n    Person.find(context.name).feed(snack);\n  }\n}\n```\n\nNow let's create our organizer like this:\n\n```javascript\nimport { Organizer } from \"@douglasgreyling/light-service\";\n\nclass GreetsAndFeedsSomeone extends Organizer {\n  static async call(name) {\n    return this.with({ name }).reduce(GreetsSomeoneAction, FeedsSomeoneAction);\n  }\n}\n\nconst result = await GreetsAndFeedsSomeone.call(\"Shaggy\");\n```\n\nAnd that's your first organizer! It ties two actions together through a static function `call`. The organizer call function takes any name and uses it to set up an initial context (this is what the `with` function does). The organizer then executes each of the actions one after another with the `reduce` function.\n\nAs your actions are executed they will add to and remove from the context you initially set up.\n\nJust like actions, organizers return the final context as their return value.\n\n```javascript\nconst result = await GreetsAndFeedsSomeone.call(\"Shaggy\");\n\nif (result.success()) {\n  console.log(\"Time to stock up on snacks!\");\n}\n\n// => \"Time to stock up on snacks!\"\n```\n\nBecause organizers generally run through complex business logic, and every action has the potential to cause a failure, testing an organizer is functionally equivalent to an integration test.\n\n## Simplifying our first tax example\n\nLet's clean up the controller we started with by using LightService.\n\nWe'll begin by looking at the controller. We want to look for distinct steps which we can separate whenever we need to update the tax on an order. By doing this we notice 3 clear processes:\n\n1. Look up the tax percentage based on order total.\n2. Calculate the order tax.\n3. Provide free shipping if the total with tax is greater than \\$200.\n\n#### The organizer\n\n```javascript\nclass CalculatesTax extends Organizer {\n  static async call(order) {\n    return this.with({ order }).reduce(\n      LooksUpTaxPercentageAction,\n      CalculatesOrderTaxAction,\n      ProvidesFreeShippingAction,\n    );\n  }\n}\n```\n\n#### Looking up the tax percentage\n\n```javascript\nclass LooksUpTaxPercentageAction extends Action {\n  expects = [\"order\"];\n  promises = [\"taxPercentage\"];\n\n  executed(context) {\n    const order = context.order;\n    const taxRanges = TaxRange.forRegion(order.region);\n\n    context.taxPercentage = 0;\n\n    if (taxRanges === undefined) {\n      context.fail(\"The tax ranges were not found\");\n      this.nextContext();\n    }\n\n    const taxPercentage = taxRanges.forTotal(order.total);\n\n    if (taxPercentage === undefined) {\n      context.fail(\"The tax percentage was not found\");\n      this.nextContext();\n    }\n\n    context.taxPercentage = taxPercentage;\n  }\n}\n```\n\n#### Calculating the order tax\n\n```javascript\nclass CalculatesOrderTaxAction extends Action {\n  expects = [\"order\", \"taxPercentage\"];\n\n  executed(context) {\n    context.order.tax = (\n      context.order.total *\n      (context.taxPercentage / 100)\n    ).toFixed(2);\n  }\n}\n```\n\n#### Providing free shipping (where applicable)\n\n```javascript\nclass ProvidesFreeShippingAction extends Action {\n  expects = [\"order\"];\n\n  executed(context) {\n    const totalWithTax = context.order.totalWithTax();\n\n    if (totalWithTax > 200) {\n      context.order.provideFreeShipping();\n    }\n  }\n}\n```\n\n#### And finally, the controller\n\n```javascript\nclass TaxController extends Controller {\n  async update(request, response) {\n    const order = Order.find(request.id);\n\n    const result = await CalculatesTax.call(order);\n\n    if (result.failure()) {\n      return ...; // render some view\n    } else {\n      return ...; // Redirect to some view with a flash message\n    }\n  }\n}\n```\n\n## Caveats\n\nLightService is really useful when you need to put together a series of functions in order to create an elegant processing pipeline. JavaScript will make this a little more challenging given that it implements asynchronous code.\n\nThis implementation assumes asynchronous code is present in your actions and organizers (even if it isn't) so that it can execute them one at a time.\n\nBecause of this your actions/organizers will **ALWAYS** return a promise.\n\n## Tips & Tricks\n\n### Stopping a series of actions\n\nWhen nothing unexpected happens during the organizer's call, the returned context will be successful. Here is how you can check for this:\n\n```javascript\nconst result = await GreetsAndFeedsSomeone.call(\"Shaggy\");\n\nif (result.success()) {\n  // Every action ran to completion.\n}\n```\n\nHowever, sometimes not everything will play out as you expect it. An external API call might not be available or some complex business logic will need to stop the processing of a series of actions. You have two options to stop the call chain:\n\n1. Failing the context\n2. Skipping the rest of the actions\n\n#### Failing the context\n\nWhen something goes wrong in an action and you want to halt the chain, you need to call `fail()` on the context object. This will push the context into a failure state (`context.failure()` will evaluate to true). The context's `fail` function takes an optional message argument describing what went wrong. To return immediately from the point of failure, call `nextContext()`.\n\nIn case you want to fail the context and stop the execution of the executed block, use the `failAndReturn('something went wrong')` function. This will immediately fail the context and cause the execute function to return.\n\nHere's an example:\n\n```javascript\nclass SubmitsOrderAction extends Action {\n  executed(context) {\n    if (!context.order.submitOrderSuccessful()) {\n      context.failAndReturn(\"Failed to submit the order\");\n    }\n\n    // This won't be executed\n    context.mailer.sendOrderNotification();\n  }\n}\n```\n\nLet's imagine that in the example above the organizer could have called 4 actions. The first 2 actions were executed until the 3rd action failed, and pushed the context into a failed state and so the 4th action was skipped.\n\n![LightService](resources/failing-the-context.png)\n\n#### Skipping the rest of the actions\n\nYou can skip the rest of the actions by calling `skipRemaining()` on the context. This behaves very similarly to the above-mentioned fail mechanism, except this will not push the context into a failure state. A good use case is running the first couple of actions and then deciding, based on what they found, that the rest are unnecessary. Here is an example of how you do it:\n\n```javascript\nclass ChecksOrderStatusAction extends Action {\n  executed(context) {\n    if (context.order.mustSendNotification()) {\n      // Everything is good, no need to execute the rest of the actions.\n      context.skipRemaining();\n    }\n  }\n}\n```\n\nLet's imagine that in the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd decided to skip the rest, so the 4th action was not invoked. The context was successful.\n\n![LightService](resources/skip-remaining.png)\n\n### Hooks\n\nIn case you need to inject code right before, after or even around actions, then hooks could be the droid you're looking for. Hooks are a great way to decouple instrumentation from business logic.\n\nConsider this code:\n\n```javascript\nclass SomeOrganizer extends Organizer {\n  static async call(context) {\n    return this.with(context).reduce(...this.actions());\n  }\n\n  static actions() {\n    return [OneAction, TwoAction, ThreeAction];\n  }\n}\n\nclass TwoAction extends Action {\n  executed(context) {\n    if (context.user.role == \"admin\")\n      context.logger.info(\"admin is doing something\");\n\n    context.user.doSomething();\n  }\n}\n```\n\nThe logging logic makes `TwoAction` more complex, there is more code for logging than for business logic.\n\nThree hooks are available for this: `beforeEach`, `afterEach` and `aroundEach`.\n\nThis is how you can declaratively add before and after hooks to the organizer:\n\n```javascript\nclass SomeOrganizer extends Organizer {\n  beforeEach(context) {\n    if (context.currentAction() == \"TwoAction\") {\n      if (context.user.role != \"admin\") return;\n\n      context.logger.info(\"admin is doing something\");\n    }\n  }\n\n  afterEach(context) {\n    if (context.currentAction() == \"TwoAction\") {\n      if (context.user.role != \"admin\") return;\n\n      context.logger.info(\"admin is doing something\");\n    }\n  }\n\n  aroundEach(context) {\n    context.logger.info(\"admin is about to do (or already has done) something\");\n  }\n\n  static async call(context) {\n    return this.with(context).reduce(...this.actions());\n  }\n\n  static actions() {\n    return [OneAction, TwoAction, ThreeAction];\n  }\n}\n\nclass TwoAction extends Action {\n  executed(context) {\n    context.user.doSomething();\n  }\n}\n```\n\nNote how the action has no logging logic after this change. Also, you can target before and after action logic for specific actions, as the `context.currentAction()` will have the class name of the currently processed action. In the example above, logging will occur only for `TwoAction` and not for `OneAction` or `ThreeAction`.\n\nHooks bracket the actions which actually run. An action skipped by an earlier\nfailure or by `skipRemaining()` gets no hooks at all, while an action which\nstarts and then fails still gets its closing `afterEach` and `aroundEach` — so\ninstrumentation is not lost precisely when something has gone wrong.\n\nHooks are declared on the organizer, and `this` inside them is the organizer.\n\n### Expects and promises\n\nThe `expects` and `promises` properties are rules for the inputs/outputs of an action. `expects` describes what keys it needs to exist inside the context for the action to execute and finish successfully. `promises` makes sure the keys are in the context after the action has been executed. If either is violated, a custom exception is thrown.\n\nThis is how it's used:\n\n```javascript\nclass FooAction extends Action {\n  expects = [\"a\", \"b\"];\n  promises = [\"c\"];\n\n  executed(context) {\n    context.c = context.a + context.b;\n  }\n}\n```\n\nExpects can also be an object. This allows you to pass additional arguments like default values:\n\n```javascript\nclass FooAction extends Action {\n  expects = { fields: [\"a\", \"b\"], defaults: { a: 1 } };\n  promises = [\"c\"];\n\n  executed(context) {\n    context.c = context.a + context.b;\n  }\n}\n```\n\nThe default is only applied when the expected key is missing from the context. A key that is present but set to `undefined` counts as present, and keeps its value.\n\nAcceptable defaults also include functions.\n\n### Context\n\nThe context returned by actions and organizers includes some handy helper functions such as the following:\n\n1. The current action (`context.currentAction();`)\n2. The current organizer (`context.currentOrganizer();`)\n3. The failure status of the context (`context.failure();`)\n4. The success status of the context (`context.success();`)\n5. The failure message if it exists (`context.message();`)\n\n#### Reserved context keys\n\nYour data and the context API share one object, so a key named after a context\nfunction would shadow it. LightService refuses those keys up front with a\n`ReservedContextKeysError` rather than letting the collision surface later:\n\n```javascript\nthis.with({ message: \"Order created\" });\n// => ReservedContextKeysError: The following context keys are reserved by\n//    LightService: message\n```\n\nThe reserved names are `success`, `failure`, `message`, `currentOrganizer`,\n`currentAction`, `errorCode`, `shouldRollback`, `fail`, `nextContext`,\n`failAndReturn`, `skipRemaining`, `failWithRollback`, `cleanActionContext`,\n`cleanOrganizerContext`, `registerAliases` and `__mapAlias`. Aliases are checked\nagainst the same list. Keys beginning with `__` are also used internally, so\navoid that prefix for your own data.\n\n`currentAction()` and `currentOrganizer()` return class names, which a minifier\nwill rewrite. If your code is bundled, declare a stable name and it will be used\ninstead:\n\n```javascript\nclass GreetsSomeoneAction extends Action {\n  static displayName = \"GreetsSomeoneAction\";\n}\n```\n\nAlso, take advantage of destructuring as much as possible. You can still refer to and mutate the context via `this` like the following:\n\n```javascript\nclass FooAction extends Action {\n  expects = [\"a\", \"b\"];\n  promises = [\"c\"];\n\n  executed({ a, b }) {\n    this.context.c = a + b;\n  }\n}\n```\n\n### Key aliases\n\nThe `aliases` property allows you to create an alias for a key found inside the organizers context. Actions can then access the context using the aliases.\n\nThis allows you to put together existing actions from different sources and have them work together without having to modify their code. Aliases will work with, or without, action expects.\n\nIf a key alias is set for a key which already holds a value in the context, an `AliasKeyAlreadyInContextError` is raised rather than quietly replacing it.\n\nSay for example you have actions `AnAction` and `AnotherAction` that you've used in previous projects. `AnAction` provides `myKey` but `AnotherAction` needs to use that key but expects it to be called `keyAlias` instead. You can use them together in an organizer like so:\n\n```javascript\nclass AnOrganizer extends Organizer {\n  aliases = { myKey: \"keyAlias\" };\n\n  static async call(order) {\n    return this.with({ order }).reduce(AnAction, AnotherAction);\n  }\n}\n\nclass AnAction extends Action {\n  promises = [\"myKey\"];\n\n  executed(context) {\n    context.myKey = \"value\";\n  }\n}\n\nclass AnotherAction extends Action {\n  expects = [\"keyAlias\"];\n\n  executed(context) {\n    context.keyAlias;\n  }\n}\n```\n\n### Error codes\n\nYou can add some more structure to your error handling by taking advantage of error codes in the context. Normally, when something goes wrong in your actions, you fail the process by setting the context to failure:\n\n```javascript\nclass SomeAction extends Action {\n  executed(context) {\n    context.fail(\"I don't like what happened here.\");\n  }\n}\n```\n\nHowever, you might need to handle the errors coming from your action pipeline differently. Using an error code can help you check what type of expected error occurred in the organizer, or in the actions.\n\n```javascript\nclass SomeAction extends Action {\n  executed(context) {\n    if (context.teapot.heat() < 95)\n      context.fail(\"The teapot is not hot enough\", { errorCode: 1234 });\n\n    // Make some tea\n\n    if (context.sugar.amount() < 2)\n      context.fail(\"There is not enough sugar for the tea\", {\n        errorCode: 5678,\n      });\n  }\n}\n```\n\nIf this action were executed, then you can pull the error message like you would normally, but you can also retrieve the error code.\n\n```javascript\nconst result = await SomeAction.execute();\n\nconsole.log(result.message());\n// => \"The teapot is not hot enough\"\n\nconsole.log(result.errorCode());\n// => 1234\n```\n\nExpectation and promise violations throw typed errors, so you can catch a\nspecific kind rather than matching on the message:\n\n```javascript\nconst {\n  ExpectedKeysNotInContextError,\n} = require(\"@douglasgreyling/light-service\");\n\ntry {\n  await SomeAction.execute();\n} catch (err) {\n  if (err instanceof ExpectedKeysNotInContextError) {\n    // A key the action expected was missing from the context.\n  }\n}\n```\n\n### Action rollback\n\nSometimes your action has to undo what it did when an error occurs. Think about a chain of actions where you need to persist records in your data store in one action and you have to call an external service in the next. What happens if there is an error when you call the external service? You want to remove the records you previously saved, which is what the `rolledBack` function is for.\n\n```javascript\nclass SaveEntities extends Action {\n  expects = [\"user\"];\n\n  executed(context) {\n    context.user.save();\n  }\n\n  rolledBack(context) {\n    context.user.destroy();\n  }\n}\n```\n\nYou need to call the `failWithRollback` function to initiate a rollback for actions starting with the action where the failure was triggered.\n\n```javascript\nclass CallSomeExternalAPI extends Action {\n  async executed(context) {\n    const apiCallResult = await SomeAPI.saveUser(context.user);\n\n    if (apiCallResult.failure())\n      context.failWithRollback(\"Error when calling external API\");\n  }\n}\n```\n\nUsing the `rolledBack` function is optional for the actions in the chain. You shouldn't care about undoing non-persisted changes.\n\nThe actions are rolled back in reversed order from the point of failure starting with the action that triggered it.\n\n## Development\n\nThe only requirement is Docker — no local Node or npm install needed.\n\n```bash\ndocker compose run --rm test     # run the suite once, with coverage\ndocker compose run --rm watch    # re-run the suite on every save\ndocker compose run --rm lint     # ESLint\ndocker compose run --rm format   # rewrite files with Prettier\ndocker compose run --rm dev      # a shell inside the container\ndocker compose run --rm ci       # run against a clean checkout, no bind mounts\n```\n\nThe first command builds the image; everything after that reuses it. Your\nworking tree is mounted live, so edits on the host are picked up immediately and\n`coverage/` is written back to the host.\n\n`node_modules` deliberately lives in a Docker volume rather than on the host, so\nyour local platform never leaks into the Linux container (and vice versa). If\n`package-lock.json` changes, the container notices and reinstalls on its next\nstart — you don't need to rebuild by hand.\n\nThe container runs Node 24 (the active LTS), which is what CI treats as the\nprimary version. CI also runs the suite against Node 22 and 26; reproduce either\nlocally by overriding `NODE_VERSION`:\n\n```bash\nNODE_VERSION=26 docker compose build\n```\n\nOn Linux, build with your own user ids so files created in the container belong\nto you:\n\n```bash\nUID=$(id -u) GID=$(id -g) docker compose build\n```\n\n### Linting and formatting\n\nESLint handles correctness, Prettier owns formatting, and the two are kept from\nfighting by `eslint-config-prettier`. CI fails on either.\n\n```bash\nnpm run lint          # report problems\nnpm run lint:fix      # fix what can be fixed automatically\nnpm run format        # rewrite files with Prettier\nnpm run format:check  # verify formatting without writing (what CI runs)\n```\n\n### Auditing dependencies\n\n```bash\nnpm audit\n```\n\nCI runs `npm audit --audit-level=high` on every push and again once a week, so\nnewly disclosed advisories surface without waiting for someone to push. The\npackage has no runtime dependencies, so this is a supply-chain check on the\ndevelopment toolchain rather than on anything shipped to users.\n\n### Working without Docker\n\n`npm install && npm test` works too — the project has no runtime dependencies,\nand the whole development toolchain is Jest, ESLint and Prettier.\n\n## Contributing\n\n1. Fork it\n2. Try keep your commits semantic [like this](https://seesparkbox.com/foundry/semantic_commit_messages).\n3. Create your feature branch (git checkout -b my-new-feature)\n4. Commit your changes (git commit -am 'fix: Added some feature')\n5. Push to the branch (git push origin my-new-feature)\n6. Create new Pull Request\n\n## License\n\nLightService is released under the MIT License.\n","readmeFilename":"README.md"}