diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..934f1aa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: Node.js CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [lts/*] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 2e4e7cc..f6fddfe 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ debug* Thumbs.db *.swp -package-lock.json stats.json benchmark-result/* + +*.tgz diff --git a/.npmignore b/.npmignore index f783b11..6a52e79 100644 --- a/.npmignore +++ b/.npmignore @@ -39,3 +39,5 @@ test/ *.js.map *.ts !*.d.ts + +*.tgz diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0ebcc2e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -dist: trusty -node_js: -- node -- lts/* diff --git a/LICENSE b/LICENSE index 7e5f38f..12bcd3f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,585 @@ -Copyright (c) 2018, Mykhailo Stadnyk - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most of +our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can +do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify it: +responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a +fee, you must pass on to the recipients the same freedoms that you received. You +must make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there +is no warranty for this free software. For both users' and authors' sake, the +GPL requires that modified versions be marked as changed, so that their problems +will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. This +is fundamentally incompatible with the aim of protecting users' freedom to +change the software. The systematic pattern of such abuse occurs in the area of +products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on +general-purpose computers, but in those that do, we wish to avoid the special +danger that patents applied to a free program could make it effectively +proprietary. To prevent this, the GPL assures that patents cannot be used to +render the program non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS +0. Definitions. +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each +licensee is addressed as “you”. “Licensees” and “recipients” may be individuals +or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a “modified version” of the earlier work or a work +“based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the +Program. + +To “propagate” a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer network, +with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent +that it includes a convenient and prominently visible feature that (1) displays +an appropriate copyright notice, and (2) tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making +modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The “System Libraries” of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A “Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and other +parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under +any applicable law fulfilling obligations under article 11 of the WIPO copyright +treaty adopted on 20 December 1996, or similar laws prohibiting or restricting +circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on each +copy an appropriate copyright notice; keep intact all notices stating that this +License and any non-permissive terms added in accord with section 7 apply to the +code; keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and + giving a relevant date. +b) The work must carry prominent notices stating that it is released under this + License and any conditions added under section 7. This requirement modifies + the requirement in section 4 to “keep intact all notices”. +c) You must license the entire work, as a whole, under this License to anyone + who comes into possession of a copy. This License will therefore apply, along + with any applicable section 7 additional terms, to the whole of the work, + and all its parts, regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not invalidate + such permission if you have separately received it. +d) If the work has interactive user interfaces, each must display Appropriate + Legal Notices; however, if the Program has interactive interfaces that do not + display Appropriate Legal Notices, your work need not make them do so. +A compilation of a covered work with other separate and independent works,which +are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an “aggregate” if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including a + physical distribution medium), accompanied by the Corresponding Source fixed + on a durable physical medium customarily used for software interchange. +b) Convey the object code in, or embodied in, a physical product (including a + physical distribution medium), accompanied by a written offer, valid for at + least three years and valid for as long as you offer spare parts or customer + support for that product model, to give anyone who possesses the object code + either (1) a copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical medium + customarily used for software interchange, for a price no more than your + reasonable cost of physically performing this conveying of source, or (2) + access to copy the Corresponding Source from a network server at no charge. +c) Convey individual copies of the object code with a copy of the written offer + to provide the Corresponding Source. This alternative is allowed only + occasionally and noncommercially, and only if you received the object code + with such an offer, in accord with subsection 6b. +d) Convey the object code by offering access from a designated place (gratis or + for a charge), and offer equivalent access to the Corresponding Source in the + same way through the same place at no further charge. You need not require + recipients to copy the Corresponding Source along with the object code. If + the place to copy the object code is a network server, the Corresponding + Source may be on a different server (operated by you or a third party) that + supports equivalent copying facilities, provided you maintain clear + directions next to the object code saying where to find the Corresponding + Source. Regardless of what server hosts the Corresponding Source, you remain + obligated to ensure that it is available for as long as needed to satisfy + these requirements. +e) Convey the object code using peer-to-peer transmission, provided you inform + other peers where the object code and Corresponding Source of the work are + being offered to the general public at no charge under subsection 6d. +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a particular +user, “normally used” refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way in which +the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version +of its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented or +interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added by +you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or +b) Requiring preservation of specified reasonable legal notices or author + attributions in that material or in the Appropriate Legal Notices displayed + by works containing it; or +c) Prohibiting misrepresentation of the origin of that material, or requiring + that modified versions of such material be marked in reasonable ways as + different from the original version; or +d) Limiting the use for publicity purposes of names of licensors or authors of + the material; or +e) Declining to grant rights under trademark law for use of some trade names, + trademarks, or service marks; or +f) Requiring indemnification of licensors and authors of that material by anyone + who conveys the material (or modified versions of it) with contractual + assumptions of liability to the recipient, for any liability that these + contractual assumptions directly impose on those licensors and authors. +All other non-permissive additional terms are considered “further restrictions” +within the meaning of section 10. If the Program as you received it, or any part +of it, contains a notice stating that it is governed by this License along with +a term that is a further restriction, you may remove that term. If a license +document contains a further restriction but permits relicensing or conveying +under this License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does not survive +such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, and +will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated (a) provisionally, unless and until +the copyright holder explicitly and finally terminates your license, and (b) +permanently, if the copyright holder fails to notify you of the violation by +some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify +to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of +the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work results +from an entity transaction, each party to that transaction who receives a copy +of the work also receives whatever licenses to the work the party's predecessor +in interest had or could give under the previous paragraph, plus a right to +possession of the Corresponding Source of the work from the predecessor in +interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To “grant” such a patent license to a party means to make such an agreement or +commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available network +server or other readily accessible means, then you must either (1) cause the +Corresponding Source to be so available, or (2) arrange to deprive yourself of +the benefit of the patent license for this particular work, or (3) arrange, in a +manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have actual +knowledge that, but for the patent license, your conveying the covered work in a +country, or your recipient's use of the covered work in a country, would +infringe one or more identifiable patents in that country that you have reason +to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients of +the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you (or +copies made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless you +entered into that arrangement, or that patent license was granted, prior to 28 +March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link +or combine any covered work with a work licensed under version 3 of the GNU +Affero General Public License into a single combined work, and to convey the +resulting work. The terms of this License will continue to apply to the part +which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License “or any later +version” applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Free Software Foundation. If the Program does not specify a version number +of the GNU General Public License, you may choose any version ever published by +the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the +GNU General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright holder +as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs +If you develop a new program, and you want it to be of the greatest possible use +to the public, the best way to achieve this is to make it free software which +everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion of +warranty; and each file should have at least the “copyright” line and a pointer +to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might be +different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if +any, to sign a “copyright disclaimer” for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider +it more useful to permit linking proprietary applications with the library. If +this is what you want to do, use the GNU Lesser General Public License instead +of this License. But first, please read +. diff --git a/README.md b/README.md index fe1085d..e44ea6b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ -# I Message Queue RPC (imq-rpc) +# I Message Queue RPC (@imqueue/rpc) -[![Build Status](https://travis-ci.org/imqueue/imq-rpc.svg?branch=master)](https://travis-ci.org/imqueue/imq-rpc) -[![codebeat badge](https://codebeat.co/badges/c2cc3954-6824-4cdf-8349-12bd9c31955a)](https://codebeat.co/projects/github-com-imqueue-imq-rpc-master) -[![Coverage Status](https://coveralls.io/repos/github/imqueue/imq-rpc/badge.svg?branch=master)](https://coveralls.io/github/imqueue/imq-rpc?branch=master) -[![License](https://img.shields.io/badge/license-ISC-blue.svg)](https://rawgit.com/imqueue/imq-rpc/master/LICENSE) +[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://rawgit.com/imqueue/rpc/master/LICENSE) RPC-like client-service implementation over messaging queue. This module provides base set of abstract classes and decorators to build services and @@ -21,7 +18,7 @@ to handle any load. ## Installation ~~~bash -npm i --save imq-rpc +npm i --save @imqueue/rpc ~~~ ## Usage @@ -38,7 +35,7 @@ types usages. File `service.ts`: ~~~typescript -import { IMQService, expose } from 'imq-rpc'; +import { IMQService, expose } from '@imqueue/rpc'; class Hello extends IMQService { @@ -88,7 +85,7 @@ There are 3 ways of building service clients: File: `client.ts` (manually written client example): ~~~typescript -import { IMQClient, IMQDelay, remote } from 'imq-rpc'; +import { IMQClient, IMQDelay, remote } from '@imqueue/rpc'; class HelloClient extends IMQClient { @@ -124,7 +121,7 @@ class HelloClient extends IMQClient { Using dynamically built clients (for the same service described above): ~~~typescript -import { IMQClient } from 'imq-rpc'; +import { IMQClient } from '@imqueue/rpc'; (async () => { try { @@ -154,7 +151,7 @@ BTW, `IMQClient.create()` supports a source code generation without a module loading as well: ~~~typescript -import { IMQClient } from 'imq-rpc'; +import { IMQClient } from '@imqueue/rpc'; (async () => { await IMQClient.create('Hello', { @@ -190,4 +187,5 @@ a first build then re-used each new build to make it work consistently. ## License -[ISC](https://github.com/imqueue/imq-rpc/blob/master/LICENSE) +This project is licensed under the GNU General Public License v3.0. +See the [LICENSE](LICENSE) diff --git a/index.ts b/index.ts index a614308..2cc6eab 100644 --- a/index.ts +++ b/index.ts @@ -1,19 +1,23 @@ /*! - * I Message Queue exports + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Copyright (c) 2018, Mykhailo Stadnyk + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './src'; -export * from 'imq'; +export * from '@imqueue/core'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6c8c493 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3938 @@ +{ + "name": "@imqueue/rpc", + "version": "2.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@imqueue/rpc", + "version": "2.1.0", + "license": "GPL-3.0-only", + "dependencies": { + "@imqueue/core": "^2.0.26", + "@types/node": "^24.9.1", + "acorn": "^8.15.0", + "farmhash": "^5.0.1", + "node-machine-id": "^1.1.12", + "reflect-metadata": "^0.2.2", + "typescript": "^5.9.3" + }, + "devDependencies": { + "@types/chai": "^5.2.2", + "@types/mocha": "^10.0.10", + "@types/mock-require": "^3.0.0", + "@types/sinon": "^17.0.4", + "chai": "^6.0.1", + "coveralls-next": "^5.0.0", + "minimist": "^1.2.8", + "mocha": "^11.7.2", + "mocha-lcov-reporter": "^1.3.0", + "mock-require": "^3.0.3", + "nyc": "^17.1.0", + "open": "^10.1.2", + "sinon": "^21.0.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "typedoc": "^0.28.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.14.0.tgz", + "integrity": "sha512-c5X8fwPLOtUS8TVdqhynz9iV0GlOtFUT1ppXYzUUlEXe4kbZ/mvMT8wXoT8kCwUka+zsiloq7sD3pZ3+QVTuNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.14.0", + "@shikijs/langs": "^3.14.0", + "@shikijs/themes": "^3.14.0", + "@shikijs/types": "^3.14.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@imqueue/core": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-2.0.26.tgz", + "integrity": "sha512-4Ecy/23+mVcSkeNYJGbcj1XaU4Hicnu9MQTLejOhuhVrgspOWrYr/GY7dDlF7amWKw1zFN8FehRFdszgz1YxPQ==", + "license": "GPL-3.0-only", + "dependencies": { + "ioredis": "^5.8.2" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz", + "integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.14.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz", + "integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.14.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz", + "integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.14.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz", + "integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mock-require": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-3.0.0.tgz", + "integrity": "sha512-TZ/s3ufaF+kEFPx9PqXKNd7OsPJoVsgOd59EA8XQLTRjJCsOdx5sEAOGhyrZ3Os78iaDKyGJWdQWyYWRg1Iyvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/coveralls-next": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-5.0.0.tgz", + "integrity": "sha512-RCj6Oflf6iQtN3Q5b0SSemEbQBzeBjQlLUrc3bfNECTy83hMJA9krdNZ5GTRm7Jpbyo92yKUbQDP5FYlWcL5sA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "js-yaml": "4.1.0", + "lcov-parse": "1.0.0", + "log-driver": "1.2.7", + "minimist": "1.2.8" + }, + "bin": { + "coveralls": "bin/coveralls.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.241", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", + "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/farmhash": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-5.0.1.tgz", + "integrity": "sha512-YSrAI+lNwDsvBsHfhYO6ihkFjObVsI/4DckifTcfGrks6MR4WxZoOtNWlHXDfzmFo4B7x0rOlxAxgxghu8qMtw==", + "license": "Apache-2.0", + "dependencies": { + "detect-libc": "^2.1.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "lcov-parse": "bin/cli.js" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=0.8.6" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz", + "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha-lcov-reporter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", + "integrity": "sha512-/5zI2tW4lq/ft8MGpYQ1nIH6yePPtIzdGeUEwFMKfMRdLfAQ1QW2c68eEJop32tNdN5srHa/E2TzB+erm3YMYA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=4.3.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "license": "MIT" + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typedoc": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz", + "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.12.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index fd7803f..fa8ff74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "imq-rpc", - "version": "1.1.2", + "name": "@imqueue/rpc", + "version": "2.1.0", "description": "RPC-like client-service implementation over messaging queue", "keywords": [ "redis", @@ -13,10 +13,10 @@ "message-queue" ], "scripts": { - "prepublish": "./node_modules/.bin/tsc", - "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov | coveralls", - "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"require('opn')('file://`pwd`/coverage/index.html');process.exit(0)\"", - "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_RPC_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"require('opn')('https://coveralls.io/github/imqueue/imq-rpc');process.exit(0)\"", + "prepublishOnly": "./node_modules/.bin/tsc", + "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov", + "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"", + "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_RPC_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq-rpc', { wait: false }));\"", "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", "clean-maps": "find . -name '*.js.map' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", "clean-js": "find . -name '*.js' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", @@ -24,51 +24,60 @@ "clean-doc": "rm -rf docs", "clean-benchmark": "rm -rf benchmark-result", "clean": "npm run clean-tests && npm run clean-typedefs && npm run clean-maps && npm run clean-js && npm run clean-doc && npm run clean-benchmark", - "doc": "rm -rf docs && typedoc --excludePrivate --excludeExternals --hideGenerator --exclude \"**/+(debug|test|node_modules|docs|coverage|benchmark|.nyc_output)/**/*\" --mode file --out ./docs . && /usr/bin/env node -e \"require('opn')('file://`pwd`/docs/index.html');process.exit(0)\"" + "doc": "rm -rf docs && typedoc --excludePrivate --excludeExternals --hideGenerator --exclude \"**/+(debug|test|node_modules|docs|coverage|benchmark|.nyc_output)/**/*\" --out ./docs . && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/docs/index.html', { wait: false }));\"" }, "repository": { "type": "git", - "url": "git@github.com/imqueue/imq-rpc.git" + "url": "git@github.com/imqueue/rpc.git" }, "bugs": { - "url": "https://github.com/imqueue/imq-rpc/issues" + "url": "https://github.com/imqueue/rpc/issues" }, "homepage": "https://imqueue.com/", - "author": "Mykhailo Stadnyk (https://github.com/Mikhus)", - "license": "ISC", + "author": "imqueue.com (https://imqueue.com)", + "license": "GPL-3.0-only", "dependencies": { - "@types/node": "^9.4.6", - "@types/acorn": "^4.0.3", - "acorn": "^5.5.3", - "acorn-es7-plugin": "^1.1.7", - "imq": "^1.2.17", - "murmurhash-native": "^3.2.2", - "node-machine-id": "^1.1.10", - "reflect-metadata": "^0.1.12", - "typescript": "^2.7.2" + "@imqueue/core": "^2.0.26", + "@types/node": "^24.9.1", + "acorn": "^8.15.0", + "farmhash": "^5.0.1", + "node-machine-id": "^1.1.12", + "reflect-metadata": "^0.2.2", + "typescript": "^5.9.3" }, "devDependencies": { - "@types/chai": "^4.1.2", - "@types/mocha": "^2.2.48", - "@types/mock-require": "^2.0.0", - "@types/sinon": "^4.3.0", - "chai": "^4.1.2", - "coveralls": "^3.0.0", - "mocha": "^5.0.1", + "@types/chai": "^5.2.2", + "@types/mocha": "^10.0.10", + "@types/mock-require": "^3.0.0", + "@types/sinon": "^17.0.4", + "chai": "^6.0.1", + "coveralls-next": "^5.0.0", + "minimist": "^1.2.8", + "mocha": "^11.7.2", "mocha-lcov-reporter": "^1.3.0", - "mock-require": "^3.0.1", - "nyc": "^11.4.1", - "opn": "^5.2.0", - "sinon": "^4.4.2", - "source-map-support": "^0.5.3", - "ts-node": "^5.0.1", - "typedoc": "^0.10.0" + "mock-require": "^3.0.3", + "nyc": "^17.1.0", + "open": "^10.1.2", + "sinon": "^21.0.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "typedoc": "^0.28.12" }, "main": "index.js", "typescript": { "definitions": "index.d.ts" }, + "mocha": { + "require": [ + "ts-node/register", + "source-map-support/register" + ], + "recursive": true, + "bail": true, + "full-trace": true + }, "nyc": { + "check-coverage": false, "extension": [ ".ts" ], @@ -82,7 +91,8 @@ "reporter": [ "html", "text", - "text-summary" + "text-summary", + "lcovonly" ] } } diff --git a/src/IMQCache.ts b/src/IMQCache.ts index 9d3bdc1..72a5cbd 100644 --- a/src/IMQCache.ts +++ b/src/IMQCache.ts @@ -1,21 +1,27 @@ /*! * IMQCache implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { ICache, ICacheAdapter } from '.'; +import { ICache, ICacheAdapter, ICacheConstructor } from '.'; /** * Generic cache registry @@ -32,7 +38,7 @@ export class IMQCache { * @param {any} options - adapter specific options * @returns {IMQCache} */ - public static register(adapter: ICacheAdapter, options?: any) { + public static register(adapter: ICacheAdapter | string, options?: any) { const self = IMQCache; if (typeof adapter === 'string') { @@ -46,13 +52,15 @@ export class IMQCache { else { // istanbul ignore else - if (!self.adapters[adapter.name]) { + if (!self.adapters[(adapter as ICacheConstructor).name]) { if (typeof adapter === 'function') { - self.adapters[(adapter).name] = new (adapter)(); + self.adapters[(adapter as ICacheConstructor).name] = + new (adapter)(); } else { - self.adapters[adapter.name] = adapter; + self.adapters[(adapter as ICache).name] = + adapter as any as ICache; } } } @@ -65,22 +73,24 @@ export class IMQCache { /** * Overrides existing adapter options with the given * - * @param { ICache | string} adapter - adapter to apply options to + * @param {ICache | string} adapter - adapter to apply options to * @param {any} options - adapter specific options * @returns {IMQCache} */ - public static apply(adapter: ICacheAdapter, options: any) { + public static apply(adapter: ICacheAdapter | string, options: any) { const self = IMQCache; if (!options) { return self; } - const name = typeof adapter === 'string' ? adapter : adapter.name; + const name = typeof adapter === 'string' + ? adapter + : (adapter as ICache).name; let opts = self.options[name] || {}; - self.options[name] = Object.assign(opts, options); + self.options[name] = { ...opts, ...options }; return self; } @@ -118,7 +128,9 @@ export class IMQCache { */ public static get(adapter: ICacheAdapter): ICache { return IMQCache.adapters[ - typeof adapter === 'string' ? adapter : adapter.name + typeof adapter === 'string' + ? adapter + : (adapter as ICache).name ]; } diff --git a/src/IMQClient.ts b/src/IMQClient.ts index e8427ec..c93d64e 100644 --- a/src/IMQClient.ts +++ b/src/IMQClient.ts @@ -1,47 +1,62 @@ /*! * IMQClient implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import IMQ, { IMessageQueue, - IMQOptions, - ILogger -} from 'imq'; + ILogger, + JsonObject, + AnyJson, + IMQ_SHUTDOWN_TIMEOUT, +} from '@imqueue/core'; import { pid, forgetPid, osUuid, DEFAULT_IMQ_CLIENT_OPTIONS, - IMQServiceOptions, IMQClientOptions, IMQRPCResponse, IMQRPCRequest, IMQDelay, remote, - Description + Description, + fileExists, + mkdir, + writeFile, + IMQMetadata, + BEFORE_HOOK_ERROR, + AFTER_HOOK_ERROR, + SIGNALS, } from '.'; -import * as fs from 'fs'; import * as ts from 'typescript'; import { EventEmitter } from 'events'; import * as vm from 'vm'; +import { CompilerOptions } from 'typescript'; +import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions'; process.setMaxListeners(10000); const tsOptions = require('../tsconfig.json').compilerOptions; -const SIGNALS: string[] = ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGBREAK']; +const RX_SEMICOLON: RegExp = /;+$/g; /** * Class IMQClient - base abstract class for service clients. @@ -50,19 +65,27 @@ export abstract class IMQClient extends EventEmitter { public readonly options: IMQClientOptions; public readonly id: number; - private readonly baseName: string; - private imq: IMessageQueue; public readonly name: string; - private readonly serviceName: string; + public readonly hostName: string; + public readonly serviceName: string; + public readonly queueName: string; + + private readonly baseName: string; + private readonly imq: IMessageQueue; + private readonly subscriptionImq: IMessageQueue; + private static singleImq: IMessageQueue & { name?: string}; private readonly logger: ILogger; - private resolvers: { [id: string]: [Function, Function] } = {}; + private resolvers: { [id: string]: [ + (data: AnyJson, res: IMQRPCResponse) => void, + (err: any, res: IMQRPCResponse) => void], + } = {}; // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected /** * Class constructor * * @constructor - * @param {Partial} options + * @param {Partial} options * @param {string} serviceName * @param {string} name */ @@ -82,25 +105,42 @@ export abstract class IMQClient extends EventEmitter { 'be instantiated directly!'); } - this.options = Object.assign({}, - DEFAULT_IMQ_CLIENT_OPTIONS, - options || /* istanbul ignore next */ {} - ); - + this.options = { ...DEFAULT_IMQ_CLIENT_OPTIONS, ...options }; this.id = pid(baseName); this.logger = this.options.logger || /* istanbul ignore next */ console; - this.name = `${baseName}-${osUuid()}-${this.id}`; + this.hostName = IMQClient.singleImq?.name || + `${osUuid()}-${this.id}:client`; + this.name = `${baseName}-${this.hostName}`; this.serviceName = serviceName || baseName.replace(/Client$/, ''); - this.imq = IMQ.create(this.name, this.options); + this.queueName = this.options.singleQueue ? this.hostName : this.name; + this.imq = this.createImq(); + this.subscriptionImq = this.createSubscriptionImq(); - const terminate = async () => { - await this.destroy(); + SIGNALS.forEach((signal: any) => process.on(signal, async () => { + this.destroy().catch(this.logger.error); // istanbul ignore next - process.nextTick(() => process.exit(0)); - }; + setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT); + })); + } + + private createImq(): IMessageQueue { + if (!this.options.singleQueue) { + return IMQ.create(this.queueName, this.options); + } + + if (!IMQClient.singleImq) { + IMQClient.singleImq = IMQ.create(this.queueName, this.options); + } - SIGNALS.forEach((signal: any) => process.on(signal, terminate)); - process.on('exit', terminate); + return IMQClient.singleImq; + } + + private createSubscriptionImq(): IMessageQueue { + if (!this.options.singleQueue) { + return this.imq; + } + + return IMQ.create(this.name, this.options); } /** @@ -108,15 +148,19 @@ export abstract class IMQClient extends EventEmitter { * * @access protected * @param {...any[]} args + * @template T * @returns {Promise} */ protected async remoteCall(...args: any[]): Promise { + const logger = this.options.logger || console; const method = args.pop(); - const from = this.name; + const from = this.queueName; const to = this.serviceName; let delay: number = 0; + let metadata: IMQMetadata | undefined; if (args[args.length - 1] instanceof IMQDelay) { + // noinspection TypeScriptUnresolvedVariable delay = args.pop().ms; // istanbul ignore if @@ -125,19 +169,77 @@ export abstract class IMQClient extends EventEmitter { } } + if (args[args.length - 1] instanceof IMQMetadata) { + metadata = args.pop(); + } + + const request: IMQRPCRequest = { + from, + method, + args, + ...(metadata ? { metadata } : {}), + } as IMQRPCRequest; + + if (typeof this.options.beforeCall === 'function') { + const beforeCall: IMQBeforeCall = ( + this.options.beforeCall as IMQBeforeCall + ).bind(this); + + try { + await beforeCall(request); + } catch (err) { + logger.warn(BEFORE_HOOK_ERROR, err); + } + } + return new Promise(async (resolve, reject) => { try { - const message: IMQRPCRequest = { from, method, args}; - const id = await this.imq.send(to, message, delay, reject); + const id = await this.imq.send(to, request, delay, reject); - this.resolvers[id] = [resolve, reject]; + this.resolvers[id] = [ + imqCallResolver(resolve, request, this), + imqCallRejector(reject, request, this), + ]; } catch (err) { // istanbul ignore next - reject(err); + imqCallRejector(reject, request, this)(err); } - }); + }) as Promise; + } + + // noinspection JSUnusedGlobalSymbols + /** + * Adds subscription to service event channel + * + * @param {(data: JsonObject) => any} handler + * @return {Promise} + */ + public async subscribe(handler: (data: JsonObject) => any): Promise { + return this.subscriptionImq.subscribe(this.serviceName, handler); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Destroys subscription channel to service + * + * @return {Promise} + */ + public async unsubscribe(): Promise { + return this.subscriptionImq.unsubscribe(); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Broadcasts given payload to all other service clients subscribed. + * So this is like client-to-clients publishing. + * + * @param {JsonObject} payload + * @return {Promise} + */ + public async broadcast(payload: JsonObject): Promise { + return this.imq.publish(payload, this.queueName); } /** @@ -145,35 +247,36 @@ export abstract class IMQClient extends EventEmitter { * * @returns {Promise} */ - public async start() { + public async start(): Promise { this.imq.on('message', (message: IMQRPCResponse) => { - process.nextTick(() => { - // the following condition below is hard to test with the - // current redis mock, BTW it was tested manually on real - // redis run - // istanbul ignore if - if (!this.resolvers[message.to]) { - // when there is no resolvers it means - // we have massage in queue which was initiated - // by some process which is broken. So we provide an - // ability to handle enqueued messages via EventEmitter - // interface - this.emit(message.request.method, message); - } - - const [ resolve, reject ] = this.resolvers[message.to] || - // istanbul ignore next - [ undefined, undefined ]; - - if (message.error) { - return reject && reject(message.error); - } - - resolve && resolve(message.data); - }); + // the following condition below is hard to test with the + // current redis mock, BTW it was tested manually on real + // redis run + // istanbul ignore if + if (!this.resolvers[message.to]) { + // when there is no resolvers it means + // we have message in queue which was initiated + // by some process which is broken. So we provide an + // ability to handle enqueued messages via EventEmitter + // interface + this.emit(message.request.method, message); + } + + const [ resolve, reject ] = this.resolvers[message.to] || []; + + // make sure no memory leaking + delete this.resolvers[message.to]; + + if (message.error) { + return reject && reject(message.error, message); + } + + resolve && resolve(message.data, message); }); - await this.imq.start(); + if (this.imq) { + await this.imq.start(); + } } // noinspection JSUnusedGlobalSymbols @@ -182,7 +285,7 @@ export abstract class IMQClient extends EventEmitter { * * @returns {Promise} */ - public async stop() { + public async stop(): Promise { await this.imq.stop(); } @@ -191,13 +294,13 @@ export abstract class IMQClient extends EventEmitter { * * @returns {Promise} */ - public async destroy() { + public async destroy(): Promise { + await this.imq.unsubscribe(); forgetPid(this.baseName, this.id, this.logger); this.removeAllListeners(); await this.imq.destroy(); } - /** * Returns service description metadata. * @@ -220,16 +323,80 @@ export abstract class IMQClient extends EventEmitter { name: string, options?: Partial ): Promise { - const clientOptions: IMQClientOptions = Object.assign({}, - DEFAULT_IMQ_CLIENT_OPTIONS, - options - ); + const clientOptions: IMQClientOptions = { + ...DEFAULT_IMQ_CLIENT_OPTIONS, + ...options, + }; return await generator(name, clientOptions); } } +/** + * Builds and returns call resolver, which supports after call optional hook + * + * @param {(...args: any[]) => void} resolve - source promise like resolver + * @param {IMQRPCRequest} req - request message + * @param {IMQClient} client - imq client + * @return {(data: any, res: IMQRPCResponse) => void} - hook-supported resolve + */ +export function imqCallResolver( + resolve: (data: any) => void, + req: IMQRPCRequest, + client: IMQClient, +): (data: any, res: IMQRPCResponse) => void { + return async (data: any, res: IMQRPCResponse) => { + const logger = client.options.logger || console; + + resolve(data); + + if (typeof client.options.afterCall === 'function') { + const afterCall: IMQAfterCall = ( + client.options.afterCall as IMQAfterCall + ).bind(client); + + try { + await afterCall(req, res); + } catch (err) { + logger.warn(AFTER_HOOK_ERROR, err); + } + } + } +} + +/** + * Builds and returns call rejector, which supports after call optional hook + * + * @param {(err: any) => void} reject - source promise like rejector + * @param {IMQRPCRequest} req - call request + * @param {IMQClient} client - imq client + * @return {(err: any) => void} - hook-supported reject + */ +export function imqCallRejector( + reject: (err: any) => void, + req: IMQRPCRequest, + client: IMQClient, +): (err: any, res?: IMQRPCResponse) => void { + return async (err: any, res: IMQRPCResponse) => { + const logger = client.options.logger || console; + + reject(err); + + if (typeof client.options.afterCall === 'function') { + const afterCall: IMQAfterCall = ( + client.options.afterCall as IMQAfterCall + ).bind(client); + + try { + await afterCall(req, res); + } catch (err) { + logger.warn(AFTER_HOOK_ERROR, err); + } + } + } +} + /** * Class GeneratorClient - generator helper class implementation * @access private @@ -263,10 +430,11 @@ async function getDescription( await client.destroy(); resolve(description); - }); + }) as Promise; } +// codebeat:disable[LOC,ABC] /** * Client generator helper function * @@ -287,32 +455,58 @@ async function generator( serviceName.substr(1); let src = `/*! - * IMQ-RPC Service: ${description.service.name} + * IMQ-RPC Service Client: ${description.service.name} + * + * I'm Queue Software Project + * Copyright (C) ${new Date().getFullYear()} imqueue.com * - * Copyright (c) ${new Date().getFullYear()}, imqueue.com + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { IMQClient, IMQDelay, Description, remote, profile } from 'imq-rpc'; +import { + IMQClient, + IMQDelay, + IMQMetadata, + remote, + profile, +} from '@imqueue/rpc'; export namespace ${namespaceName} {\n`; for (let typeName of Object.keys(description.types)) { - src += ` export interface ${typeName} {\n`; + src += ` export interface ${typeName} ${ + description.types[typeName].inherits && + description.types[description.types[typeName].inherits] + ? `extends ${description.types[typeName].inherits}` + : '' + } {\n`; + + const indexType = description.types[typeName].indexType; - for (let propertyName in description.types[typeName]) { + if (indexType) { + src += ' '.repeat(8); + src += `${indexType.trim().replace(RX_SEMICOLON, '').trim()};\n`; + } + + for (const propertyName of Object.keys( + description.types[typeName].properties, + )) { const { type, isOptional } = - description.types[typeName][propertyName]; + description.types[typeName].properties[propertyName]; src += ' '.repeat(8); src += `${propertyName}${isOptional ? '?' : ''}: ${type};\n`; @@ -325,32 +519,41 @@ export namespace ${namespaceName} {\n`; const methods = description.service.methods; - for (let methodName of Object.keys(methods)) { + for (const methodName of Object.keys(methods)) { if (methodName === 'describe') { continue; // do not create inherited method - no need } - let args = methods[methodName].arguments; - let description = methods[methodName].description; - let ret = methods[methodName].returns; - let retType = ret.tsType - .replace(/\r?\n/g, ' ') - .replace(/\s{2,}/g, ' '); - - // make sure each method allows optional delay argument - const lastArg = args[args.length - 1]; - - if (lastArg && lastArg.type !== 'IMQDelay') { - args.push({ - description: 'if passed the method will be called with ' + - 'the specified delay over message queue', - name: 'delay', - type: 'IMQDelay', - tsType: 'IMQDelay', - isOptional: true - }); + const args = methods[methodName].arguments; + const description = methods[methodName].description; + const ret = methods[methodName].returns; + const addArgs = [{ + description: 'if passed, will deliver given metadata to ' + + 'service, and will initiate trace handler calls', + name: 'imqMetadata', + type: 'IMQMetadata', + tsType: 'IMQMetadata', + isOptional: true, + }, { + description: 'if passed the method will be called with ' + + 'the specified delay over message queue', + name: 'imqDelay', + type: 'IMQDelay', + tsType: 'IMQDelay', + isOptional: true + }]; + let retType = ret.tsType.replace(/\r?\n/g, ' ').replace(/\s{2,}/g, ' '); + + for (let i = 1; i <= 2; i++) { + const arg = args[args.length - i]; + + if (arg && ~['IMQDelay', 'IMQMetadata'].indexOf(arg.type)) { + args.pop(); // remove it + } } + args.push(...addArgs); // make sure client expect them + // istanbul ignore if if (retType === 'Promise') { retType = 'Promise'; @@ -363,7 +566,7 @@ export namespace ${namespaceName} {\n`; .join('\n') + '\n *\n' : ''; for (let i = 0, s = args.length; i < s; i++) { - let arg = args[i]; + const arg = args[i]; src += ` * @param {${ toComment(arg.tsType)}} `; src += arg.isOptional ? `[${arg.name}]` : arg.name; @@ -378,7 +581,7 @@ export namespace ${namespaceName} {\n`; src += ` public async ${methodName}(`; for (let i = 0, s = args.length; i < s; i++) { - let arg = args[i]; + const arg = args[i]; src += arg.name + (arg.isOptional ? '?' : '') + ': ' + arg.tsType.replace(/\s{2,}/g, ' ') + (i === s - 1 ? '': ', '); @@ -397,6 +600,7 @@ export namespace ${namespaceName} {\n`; return module ? module[namespaceName] : /* istanbul ignore next */ null; } +// codebeat:enable[LOC,ABC] /** * Return promised typedef of a given type if its missing @@ -453,30 +657,39 @@ function toComment(typedef: string, promised: boolean = false): string { * @param {IMQClientOptions} options * @returns {any} */ -async function compile(name: string, src: string, options: IMQClientOptions): Promise { +async function compile( + name: string, + src: string, + options: IMQClientOptions, +): Promise { const path = options.path; const srcFile = `${path}/${name}.ts`; const jsFile = `${path}/${name}.js`; + const js = ts.transpile(src, tsOptions as CompilerOptions | undefined); - const js = ts.transpile(src, tsOptions); - if (options.write) { - // istanbul ignore else - if (!fs.existsSync(path)) { - fs.mkdirSync(path); - } + if (options.write) { + // istanbul ignore else + if (!await fileExists(path)) { + await mkdir(path); + } - fs.writeFileSync(srcFile, src, { encoding: 'utf8' }); - fs.writeFileSync(jsFile, js, { encoding: 'utf8' }); - } + await Promise.all([ + writeFile(srcFile, src), + writeFile(jsFile, js), + ]); + } // istanbul ignore else if (options.compile) { const script = new vm.Script(js); const context = { exports: {}, require }; - script.runInNewContext(context, { filename: jsFile }); + + script.runInNewContext(context, { filename: jsFile }); + return context.exports; } // istanbul ignore next return null; } + diff --git a/src/IMQDelay.ts b/src/IMQDelay.ts index 6fb3688..c5529d8 100644 --- a/src/IMQDelay.ts +++ b/src/IMQDelay.ts @@ -1,19 +1,25 @@ /*! * IMQDelay implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export class IMQDelay { public get ms() { diff --git a/src/IMQLock.ts b/src/IMQLock.ts index 215627a..2a3d7f3 100644 --- a/src/IMQLock.ts +++ b/src/IMQLock.ts @@ -1,26 +1,39 @@ /*! * IMQLock implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ - -import { ILogger } from 'imq'; +import { ILogger } from '@imqueue/core'; export type AcquiredLock = T | boolean; -export type IMQLockTask = [Function, Function]; +export type IMQLockTask = [(...args: any[]) => any, (...args: any[]) => any]; export type IMQLockQueue = Array; +export interface IMQLockMetadataItem { + className: string; + methodName: string | symbol; + args: any[]; +} +export interface IMQLockMetadata { + [key: string]: IMQLockMetadataItem; +} /** * Class IMQLock. @@ -67,6 +80,7 @@ export class IMQLock { private static acquiredLocks: { [key: string]: boolean } = {}; private static queues: { [key: string]: IMQLockQueue } = {}; + private static metadata: IMQLockMetadata = {}; /** * Deadlock timeout in milliseconds @@ -86,15 +100,21 @@ export class IMQLock { * Acquires a lock for a given key * * @param {string} key - * @param {Function} callback + * @param {(...args: any[]) => any} [callback] + * @param {IMQLockMetadataItem} [metadata] * @returns {AcquiredLock} */ public static async acquire( key: string, - callback?: Function + callback?: (...args: any[]) => any, + metadata?: IMQLockMetadataItem, ): Promise> { IMQLock.queues[key] = IMQLock.queues[key] || []; + if (metadata) { + IMQLock.metadata[key] = metadata; + } + if (IMQLock.locked(key)) { return new Promise((resolve, reject) => { let timer: any = null; @@ -103,8 +123,17 @@ export class IMQLock { if (IMQLock.deadlockTimeout) { // avoid dead-locks using timeouts timer = setTimeout(() => { - const err = new Error( - `Lock timeout, "${key}" call rejected`); + let dumpStr = ''; + + try { + dumpStr = JSON.stringify(IMQLock.metadata[key]); + } catch (err) { + dumpStr = 'Unable to stringify metadata'; + } + + const err = new Error(`Lock timeout, "${ + key + }" call rejected, metadata: ${ dumpStr }`); clearTimeout(timer); timer = null; @@ -159,12 +188,13 @@ export class IMQLock { IMQLock.queues[key] = []; delete IMQLock.acquiredLocks[key]; + delete IMQLock.metadata[key]; let task: IMQLockTask | undefined; const processor = err ? 1 : 0; const arg = err ? err : value; - while (task = queue.shift()) { + while ((task = queue.shift())) { task[processor](arg); } } diff --git a/src/IMQMetadata.ts b/src/IMQMetadata.ts new file mode 100644 index 0000000..ae90722 --- /dev/null +++ b/src/IMQMetadata.ts @@ -0,0 +1,34 @@ +/*! + * IMQMetadata implementation + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { AnyJson, JsonObject } from '@imqueue/core'; + +export class IMQMetadata { + [property: string]: AnyJson; + + constructor(metadata: JsonObject) { + for (const property of Object.keys(metadata)) { + this[property] = metadata[property]; + } + } +} diff --git a/src/IMQRPCDescription.ts b/src/IMQRPCDescription.ts index c984650..8bccaf8 100644 --- a/src/IMQRPCDescription.ts +++ b/src/IMQRPCDescription.ts @@ -1,21 +1,26 @@ /*! * IMQRPCDescription implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ - /** * Method argument description */ @@ -86,7 +91,11 @@ export interface TypeDescription { * Entire service types metadata structure */ export interface TypesDescription { - [typeName: string]: TypeDescription; + [typeName: string]: { + properties: TypeDescription, + inherits: string, + indexType?: string, + }; } export class IMQRPCDescription { diff --git a/src/IMQRPCError.ts b/src/IMQRPCError.ts index 1bbe01a..68e40eb 100644 --- a/src/IMQRPCError.ts +++ b/src/IMQRPCError.ts @@ -1,42 +1,54 @@ /*! * IMQ-RPC Interfaces: IMQRPCError * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { IJson } from 'imq'; +import { JsonObject } from '@imqueue/core'; + +export const BEFORE_HOOK_ERROR = 'Before call hook error:'; +export const AFTER_HOOK_ERROR = 'After call hook error:'; /** * Response error data structure, which service returns if error * occurred during service method execution. */ -export interface IMQRPCError extends IJson { +export interface IMQRPCError extends JsonObject { code: string; message: string; stack: string; method: string; args: string; + original?: any; } // istanbul ignore next /** + * Builds JSON representation of IMQ Error * - * @param {string} code - * @param {string} message - * @param {string} stack - * @param {string} method - * @param {any} args + * @param {string} code - error code + * @param {string} message - error message + * @param {string} stack - error stack + * @param {string} method - IMQ service method called, which produced an error + * @param {any} args - IMQ service method call args passed + * @param {any} [original] - Original error thrown (JSON'ified) if any * @return {IMQRPCError} */ export function IMQError( @@ -44,13 +56,21 @@ export function IMQError( message: string, stack: any, method: any, - args: any + args: any, + original?: any, ): IMQRPCError { return { code, message, stack: stack || '', method: method || '', - args: JSON.stringify(args, null, 2) + args: JSON.stringify(args, null, 2), + original: (() => { + try { + return JSON.stringify(original); + } catch (err) { + return undefined; + } + })(), }; } diff --git a/src/IMQRPCOptions.ts b/src/IMQRPCOptions.ts index b8e3c8a..425f020 100644 --- a/src/IMQRPCOptions.ts +++ b/src/IMQRPCOptions.ts @@ -1,32 +1,62 @@ /*! * IMQ-RPC Interfaces: IMQServiceOptions * - * Copyright (c) 2018, Mykhailo Stadnyk - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { DEFAULT_IMQ_OPTIONS, IMQOptions } from 'imq'; +import { DEFAULT_IMQ_OPTIONS, IMQOptions } from '@imqueue/core'; +import { IMQRPCRequest } from './IMQRPCRequest'; +import { IMQRPCResponse } from './IMQRPCResponse'; +import { IMQService } from './IMQService'; +import { IMQClient } from './IMQClient'; + +export interface IMQBeforeCall { + (req?: IMQRPCRequest, res?: IMQRPCResponse): Promise; +} + +export interface IMQAfterCall { + (req: IMQRPCRequest, res?: IMQRPCResponse): Promise; +} + +export interface IMQMetricsServerOptions { + enabled?: boolean; + port?: number; + queueLengthFormatter?: (length: number, metricName: string) => string, +} export interface IMQServiceOptions extends IMQOptions { multiProcess: boolean; childrenPerCore: number; + metricsServer?: IMQMetricsServerOptions; + beforeCall?: IMQBeforeCall; + afterCall?: IMQAfterCall; } export interface IMQClientOptions extends IMQOptions { - path: string, - compile: boolean, - timeout: number, - write: boolean, + path: string; + compile: boolean; + timeout: number; + write: boolean; + beforeCall?: IMQBeforeCall; + afterCall?: IMQAfterCall; + singleQueue?: boolean; } /** @@ -34,19 +64,39 @@ export interface IMQClientOptions extends IMQOptions { * * @type {IMQServiceOptions} */ -export const DEFAULT_IMQ_SERVICE_OPTIONS: IMQServiceOptions = Object.assign({ +export const DEFAULT_IMQ_SERVICE_OPTIONS: IMQServiceOptions = { + ...DEFAULT_IMQ_OPTIONS, + cleanup: true, + cleanupFilter: '*:client', multiProcess: false, - childrenPerCore: 1 -}, DEFAULT_IMQ_OPTIONS); + childrenPerCore: 1, +}; + +/** + * Default metric server options + * + * @type {NonNullable} + */ +export const DEFAULT_IMQ_METRICS_SERVER_OPTIONS: NonNullable< + IMQMetricsServerOptions +> = { + enabled: false, + port: 9090, + queueLengthFormatter: (length, metricName) => + `${ metricName }{} ${ length }`, +}; /** * Default client options * * @type {IMQClientOptions} */ -export const DEFAULT_IMQ_CLIENT_OPTIONS: IMQClientOptions = Object.assign({ +export const DEFAULT_IMQ_CLIENT_OPTIONS: IMQClientOptions = { + ...DEFAULT_IMQ_OPTIONS, + cleanup: true, + cleanupFilter: '*:client', path: './src/clients', compile: true, timeout: 30000, write: true, -}, DEFAULT_IMQ_OPTIONS); +}; diff --git a/src/IMQRPCRequest.ts b/src/IMQRPCRequest.ts index 43b65e6..36a64a8 100644 --- a/src/IMQRPCRequest.ts +++ b/src/IMQRPCRequest.ts @@ -1,27 +1,35 @@ /*! * IMQ-RPC Interfaces: IMQRPCRequest * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { IJson } from 'imq'; +import { JsonObject } from '@imqueue/core'; +import { IMQMetadata } from './IMQMetadata'; /** * Request message data structure expected to be handled by a service */ -export interface IMQRPCRequest extends IJson { +export interface IMQRPCRequest extends JsonObject { from: string; method: string; args: any[]; + metadata?: IMQMetadata; } diff --git a/src/IMQRPCResponse.ts b/src/IMQRPCResponse.ts index 4c2210c..f24c493 100644 --- a/src/IMQRPCResponse.ts +++ b/src/IMQRPCResponse.ts @@ -1,30 +1,36 @@ /*! * IMQ-RPC Interfaces: IMQRPCResponse * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { IJson } from 'imq'; +import { JsonObject } from '@imqueue/core'; import { IMQRPCError, IMQRPCRequest } from '.'; /** * Response message data structure, which service replies to handled * requests. */ -export interface IMQRPCResponse extends IJson { +export interface IMQRPCResponse extends JsonObject { to: string; - data: IJson | null; + data: JsonObject | null; error: IMQRPCError | null; request: IMQRPCRequest } diff --git a/src/IMQRequestContext.ts b/src/IMQRequestContext.ts new file mode 100644 index 0000000..1c9d28e --- /dev/null +++ b/src/IMQRequestContext.ts @@ -0,0 +1,57 @@ +/*! + * IMQRequestContext implementation + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { IMQMetadata } from './IMQMetadata'; +import { IMQRPCRequest } from './IMQRPCRequest'; + +const storage = new AsyncLocalStorage(); + +/** + * Runs the given function with `request` bound to the current async execution + * context, so any code reached from it (and its asynchronous continuations) can + * access the in-flight request's metadata via `currentMetadata()` without + * threading it through call signatures. + * + * The binding is scoped to the function: it is established for the duration of + * the call and automatically removed afterwards, which keeps concurrent + * requests isolated from one another. + * + * @param {IMQRPCRequest} request - the request to bind for this execution + * @param {() => T} fn - the work to run within the bound context + * @return {T} + */ +export function runWithRequest(request: IMQRPCRequest, fn: () => T): T { + return storage.run(request, fn); +} + +/** + * Returns the metadata of the in-flight IMQ request for the current async + * execution, if any. Returns `undefined` outside of a `runWithRequest()` scope. + * The transport carries metadata as an opaque bag; callers interpret its fields. + * + * @return {IMQMetadata | undefined} + */ +export function currentMetadata(): IMQMetadata | undefined { + return storage.getStore()?.metadata; +} diff --git a/src/IMQService.ts b/src/IMQService.ts index 6298a5f..11d6c81 100644 --- a/src/IMQService.ts +++ b/src/IMQService.ts @@ -1,21 +1,33 @@ /*! * IMQService implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import IMQ, { ILogger, IMessageQueue, profile } from 'imq'; +import IMQ, { + JsonObject, + ILogger, + IMessageQueue, + profile, + IMQ_SHUTDOWN_TIMEOUT, +} from '@imqueue/core'; import { TypesDescription, IMQRPCDescription, @@ -27,11 +39,18 @@ import { ICache, ServiceClassDescription, MethodsCollectionDescription, - DEFAULT_IMQ_SERVICE_OPTIONS + DEFAULT_IMQ_SERVICE_OPTIONS, + AFTER_HOOK_ERROR, + BEFORE_HOOK_ERROR, + SIGNALS, DEFAULT_IMQ_METRICS_SERVER_OPTIONS, } from '.'; -import * as cluster from 'cluster'; import * as os from 'os'; -import { ArgDescription } from "./IMQRPCDescription"; +import { ArgDescription } from './IMQRPCDescription'; +import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions'; +import { runWithRequest } from './IMQRequestContext'; +import * as http from 'node:http'; + +const cluster: any = require('cluster'); export class Description { service: { @@ -41,7 +60,7 @@ export class Description { types: TypesDescription; } -let serviceDescription: Description | null = null; +const serviceDescriptions: Map = new Map(); /** * Returns collection of class methods metadata even those are inherited @@ -68,13 +87,13 @@ function getClassMethods(className: string): MethodsCollectionDescription { } /** - * Checks if given args match given args description at least by args count + * Checks if given args match the given args description at least by args count * * @param {ArgDescription[]} argsInfo * @param {any[]} args * @returns {boolean} */ -function isValidArgsCount(argsInfo: ArgDescription[], args: any[]) { +function isValidArgsCount(argsInfo: ArgDescription[], args: any[]): boolean { // istanbul ignore next return (argsInfo.some(argInfo => argInfo.isOptional) ? argsInfo.length >= args.length @@ -93,6 +112,7 @@ export abstract class IMQService { protected imq: IMessageQueue; protected logger: ILogger; protected cache: ICache; + protected metricsServer?: http.Server; public name: string; public options: IMQServiceOptions; @@ -113,18 +133,30 @@ export abstract class IMQService { 'be instantiated directly!'); } - this.options = Object.assign({}, - DEFAULT_IMQ_SERVICE_OPTIONS, - options || {} - ); - - this.logger = this.options.logger || - // istanbul ignore next - console; - this.imq = IMQ.create(this.name, options); + this.options = { + ...DEFAULT_IMQ_SERVICE_OPTIONS, + ...options, + metricsServer: { + ...DEFAULT_IMQ_METRICS_SERVER_OPTIONS, + ...(options?.metricsServer || {}), + }, + }; + this.logger = this.options.logger || /* istanbul ignore next */ console; + this.imq = IMQ.create(this.name, this.options); this.handleRequest = this.handleRequest.bind(this); + SIGNALS.forEach((signal: any) => process.on(signal, async () => { + this.destroy().catch(this.logger.error); + + if (this.metricsServer) { + this.metricsServer.close(); + } + + // istanbul ignore next + setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT); + })); + this.imq.on('message', this.handleRequest); } @@ -132,21 +164,44 @@ export abstract class IMQService { * Handles incoming request and produces corresponding response * * @access private - * @param {IMQRPCRequest} msg - request message + * @param {IMQRPCRequest} request - request message * @param {string} id - message unique identifier * @return {Promise} */ - private async handleRequest(msg: IMQRPCRequest, id: string) { - const method = msg.method; + private async handleRequest( + request: IMQRPCRequest, + id: string, + ): Promise { + return runWithRequest(request, () => this.processRequest(request, id)); + } + + private async processRequest( + request: IMQRPCRequest, + id: string, + ): Promise { + const logger = this.options.logger || console; + const method = request.method; const description = await this.describe(); - const args = msg.args; + const args = request.args; let response: IMQRPCResponse = { to: id, data: null, error: null, - request: msg + request: request, }; + if (typeof this.options.beforeCall === 'function') { + const beforeCall: IMQBeforeCall = ( + this.options.beforeCall as IMQBeforeCall + ).bind(this); + + try { + await beforeCall(request, response); + } catch (err) { + logger.warn(BEFORE_HOOK_ERROR, err); + } + } + if (!this[method]) { response.error = IMQError( 'IMQ_RPC_NO_METHOD', @@ -155,10 +210,21 @@ export abstract class IMQService { } else if (!description.service.methods[method]) { - response.error = IMQError( - 'IMQ_RPC_NO_ACCESS', - `Access to ${this.name}.${method}() denied!`, - new Error().stack, method, args); + // Allow calling runtime-attached methods (own props) even if + // they are not present in the exposed service description. + // Deny access for prototype (class) methods not decorated with @expose. + const isOwn = Object.prototype.hasOwnProperty.call(this, method); + const value: any = (this as any)[method]; + const proto = Object.getPrototypeOf(this); + const protoValue = proto && proto[method]; + const isSameAsProto = typeof protoValue === 'function' && value === protoValue; + // Allow only truly dynamic own-instance functions (not the same as prototype) + if (!(isOwn && typeof value === 'function' && !isSameAsProto)) { + response.error = IMQError( + 'IMQ_RPC_NO_ACCESS', + `Access to ${this.name}.${method}() denied!`, + new Error().stack, method, args); + } } else if (!isValidArgsCount( @@ -173,7 +239,8 @@ export abstract class IMQService { if (response.error) { this.logger.warn(response.error); - return await this.imq.send(msg.from, response); + + return await send(request, response, this); } try { @@ -186,21 +253,21 @@ export abstract class IMQService { } catch (err) { - response.error = IMQError('IMQ_RPC_CALL_ERROR', - err.message, err.stack, method, args); + response.error = IMQError(err.code || 'IMQ_RPC_CALL_ERROR', + err.message, err.stack, method, args, err); } - return await this.imq.send(msg.from, response); + return await send(request, response, this); } /** * Initializes this instance of service and starts handling request * messages. * - * @return {Promise} + * @return {Promise} */ @profile() - public async start() { + public async start(): Promise { if (!this.options.multiProcess) { this.logger.info( '%s: starting single-worker, pid %s', @@ -209,7 +276,7 @@ export abstract class IMQService { this.describe(); - return this.imq.start(); + return this.startWithMetricsServer(); } if (cluster.isMaster) { @@ -226,7 +293,7 @@ export abstract class IMQService { this.logger.info( '%s: worker pid %s died, exiting', this.name, - worker.process.pid + worker.process.pid, ); process.exit(1); }); @@ -237,13 +304,25 @@ export abstract class IMQService { '%s: worker #%s started, pid %s', this.name, process.env['workerId'], - process.pid + process.pid, ); this.describe(); - return this.imq.start(); + return this.startWithMetricsServer(); } + + return this.startWithMetricsServer(); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Sends given data to service subscription channel + * + * @param {JsonObject} data + */ + public async publish(data: JsonObject) { + await this.imq.publish(data); } /** @@ -252,7 +331,7 @@ export abstract class IMQService { * @return {Promise} */ @profile() - public async stop() { + public async stop(): Promise { await this.imq.stop(); } @@ -262,7 +341,8 @@ export abstract class IMQService { * @return {Promise} */ @profile() - public async destroy() { + public async destroy(): Promise { + await this.imq.unsubscribe(); await this.imq.destroy(); } @@ -271,20 +351,96 @@ export abstract class IMQService { * * @returns {Promise} */ - @profile() @expose() public describe(): Description { - if (!serviceDescription) { - serviceDescription = { + let description = serviceDescriptions.get(this.name) || null; + + if (!description) { + description = { service: { name: this.name, methods: getClassMethods(this.constructor.name) }, types: IMQRPCDescription.typesDescription }; + + serviceDescriptions.set(this.name, description); + } + + return description; + } + + private async startWithMetricsServer(): Promise { + const service = this.imq.start(); + const metricServerOptions = this.options.metricsServer; + + if (!(metricServerOptions && metricServerOptions.enabled)) { + return service; } - return serviceDescription; + this.logger.log('Starting metrics server...'); + + this.metricsServer = http.createServer(async (req, res) => { + if (req.url === '/metrics') { + const length = await this.imq.queueLength(); + const content = metricServerOptions.queueLengthFormatter?.( + length, 'queue_length', + ) || String(length); + + res.setHeader('Content-Type', 'plain/text'); + res.setHeader('Content-Length', Buffer.byteLength(content)); + res.writeHead(200); + res.end(content); + + return; + } + + res.writeHead(404); + res.end(); + }); + this.metricsServer.listen( + metricServerOptions.port, + '0.0.0.0', + () => { + this.logger.info( + '%s: metrics server started on port %s', + this.name, + metricServerOptions.port, + ); + }, + ); + + return service; + } +} + +/** + * Sends IMQ response with support of after call optional hook + * + * @param {IMQRPCRequest} request - from message identifier + * @param {IMQRPCResponse} response - response to send + * @param {IMQService} service - imq service to bind + * @return {Promise} - send result message identifier + */ +export async function send( + request: IMQRPCRequest, + response: IMQRPCResponse, + service: IMQService, +): Promise { + const logger = service.options.logger || console; + const id = await (service as any).imq.send(request.from, response); + + if (typeof service.options.afterCall === 'function') { + const afterCall: IMQAfterCall = ( + service.options.afterCall as IMQAfterCall + ).bind(service); + + try { + await afterCall(request, response); + } catch (err) { + logger.warn(AFTER_HOOK_ERROR, err); + } } + return id; } diff --git a/src/cache/ICache.ts b/src/cache/ICache.ts index 30e00b1..2599a1b 100644 --- a/src/cache/ICache.ts +++ b/src/cache/ICache.ts @@ -1,19 +1,25 @@ /*! * IMQCache interfaces * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export interface ICacheConstructor { new (name?: string): ICache; @@ -28,6 +34,7 @@ export interface ICache { get(key: string): Promise; set(key: string, value: any, ttl?: number): Promise; del(key: string): Promise; + purge(keyMask: string): Promise; } diff --git a/src/cache/RedisCache.ts b/src/cache/RedisCache.ts index fd5b9e1..29788f1 100644 --- a/src/cache/RedisCache.ts +++ b/src/cache/RedisCache.ts @@ -1,44 +1,52 @@ /*! * RedisCache adapter implementation * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { ICache } from '.'; import { ILogger, DEFAULT_IMQ_OPTIONS, - redis, IRedisClient, - IMQOptions -} from 'imq'; + IMQOptions, + Redis, +} from '@imqueue/core'; import * as os from 'os'; export interface IRedisCacheOptions extends Partial { - conn?: IRedisClient + conn?: IRedisClient; } -export const DEFAULT_REDIS_CACHE_OPTIONS = Object.assign( - {}, DEFAULT_IMQ_OPTIONS, { - prefix: 'imq-cache' - }); +export const DEFAULT_REDIS_CACHE_OPTIONS = { + ...DEFAULT_IMQ_OPTIONS, + prefix: 'imq-cache', +}; + +export const REDIS_CLIENT_INIT_ERROR = 'Redis client is not initialized!'; /** * Class RedisCache. Implements cache engine over redis. */ export class RedisCache implements ICache { - private static redis: IRedisClient; + private static redis?: IRedisClient; private logger: ILogger; public options: IRedisCacheOptions; public name: string = RedisCache.name; @@ -47,15 +55,14 @@ export class RedisCache implements ICache { /** * Initializes cache instance * - * @param {IRedisCacheOptions} options + * @param {IRedisCacheOptions} [options] * @returns {Promise} */ public async init(options?: IRedisCacheOptions): Promise { - this.options = Object.assign( - {}, DEFAULT_REDIS_CACHE_OPTIONS, options || - // istanbul ignore next - {} - ); + this.options = { + ...DEFAULT_REDIS_CACHE_OPTIONS, + ...options, + }; this.logger = this.options.logger || // istanbul ignore next @@ -67,15 +74,27 @@ export class RedisCache implements ICache { if (this.options.conn) { this.logger.info('Re-using given connection for cache.'); + RedisCache.redis = this.options.conn; + return this; } return new Promise((resolve, reject) => { - RedisCache.redis = redis.createClient( - Number(this.options.port), - String(this.options.host) - ); + const connectionName = `${ + this.options.prefix}:${ + this.name }:pid:${ + process.pid }:host:${ + os.hostname() + }`; + + RedisCache.redis = new Redis({ + port: Number(this.options.port), + host: String(this.options.host), + username: this.options.username, + password: this.options.password, + connectionName, + }); RedisCache.redis.on('ready', async () => { this.logger.info( @@ -83,13 +102,7 @@ export class RedisCache implements ICache { this.name, this.options.host, this.options.port, - process.pid - ); - - await RedisCache.redis.client( - 'setname', - `${this.options.prefix}:${this.name - }:pid:${process.pid}:host:${os.hostname()}` + process.pid, ); this.ready = true; @@ -116,7 +129,7 @@ export class RedisCache implements ICache { * @param {string} key * @returns {string} */ - private key(key: string) { + private key(key: string): string { return `${this.options.prefix}:${this.name}:${key}`; } @@ -127,6 +140,11 @@ export class RedisCache implements ICache { * @returns {Promise} */ public async get(key: string): Promise { + if (!RedisCache.redis) { + throw new TypeError(REDIS_CLIENT_INIT_ERROR); + } + + // noinspection ES6RedundantAwait const data = await RedisCache.redis.get(this.key(key)); if (data) { @@ -140,7 +158,7 @@ export class RedisCache implements ICache { * Stores in cache given value under given key. If TTL is specified, * cached value will expire in a given number of milliseconds. If NX * argument set to true will create key:value in cache only if it does - * not exists yet. Given value could be any JSON-compatible object and + * not exist yet. Given value could be any JSON-compatible object and * will be serialized automatically. * * @param {string} key @@ -155,6 +173,10 @@ export class RedisCache implements ICache { ttl?: number, nx: boolean = false ): Promise { + if (!RedisCache.redis) { + throw new TypeError(REDIS_CLIENT_INIT_ERROR); + } + const args: any[] = [ this.key(key), JSON.stringify(value && value.then ? await value : value) @@ -178,7 +200,39 @@ export class RedisCache implements ICache { * @returns {Promise} */ public async del(key: string): Promise { - return await RedisCache.redis.del(this.key(key)); + if (!RedisCache.redis) { + throw new TypeError(REDIS_CLIENT_INIT_ERROR); + } + + return !!await RedisCache.redis.del(this.key(key)); + } + + /** + * Purges all keys from cache by a given wildcard mask + * + * @param {string} keyMask + * @return {boolean} + */ + public async purge(keyMask: string): Promise { + if (!RedisCache.redis) { + throw new TypeError(REDIS_CLIENT_INIT_ERROR); + } + + try { + // noinspection SpellCheckingInspection + await RedisCache.redis.eval( + `for _,k in ipairs(redis.call('keys','${ + keyMask + }')) do redis.call('del',k) end`, + 0, + ); + + return true; + } catch (e) { + this.logger.error(e); + + return false; + } } /** @@ -186,13 +240,13 @@ export class RedisCache implements ICache { * * @returns {Promise} */ - public static async destroy() { + public static async destroy(): Promise { try { // istanbul ignore else if (RedisCache.redis) { RedisCache.redis.removeAllListeners(); - RedisCache.redis.end(false); - RedisCache.redis.unref(); + RedisCache.redis.disconnect(false); + RedisCache.redis.quit(); delete RedisCache.redis; } } diff --git a/src/cache/index.ts b/src/cache/index.ts index ce332c2..03505d3 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -1,17 +1,25 @@ /*! - * Copyright (c) 2018, Mykhailo Stadnyk + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './ICache'; export * from './RedisCache'; diff --git a/src/decorators/cache.ts b/src/decorators/cache.ts index 7c29aaf..7228c13 100644 --- a/src/decorators/cache.ts +++ b/src/decorators/cache.ts @@ -1,19 +1,25 @@ /*! * IMQ-RPC Decorators: cache * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { IMQCache, ICache, RedisCache, ICacheConstructor, signature } from '..'; @@ -24,23 +30,24 @@ export interface CacheDecoratorOptions { } export interface CacheDecorator { - (options?: CacheDecoratorOptions): Function; + (options?: CacheDecoratorOptions): (...args: any[]) => any; globalOptions?: CacheDecoratorOptions; } +// codebeat:disable[BLOCK_NESTING] export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) { - const cacheOptions: CacheDecoratorOptions = - Object.assign({}, cache.globalOptions, options || {}); + const cacheOptions: CacheDecoratorOptions = { + ...cache.globalOptions, + ...options, + }; let Adapter: any = cacheOptions.adapter || RedisCache; return function( target: any, methodName: string | symbol, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) { - const original = descriptor.value || - // istanbul ignore next - (() => {}); + const original: (...args: any[]) => any = descriptor.value as any; descriptor.value = async function(...args: any[]) { const context: any = this; @@ -65,7 +72,7 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) { (context.imq && context.imq.logger); if (logger) { - opts = Object.assign(opts || {}, { logger }); + opts = { ...opts, logger }; } await IMQCache.register(Adapter, opts).init(); @@ -86,7 +93,7 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) { key, result, cacheOptions.ttl, - !!cacheOptions.nx + !!cacheOptions.nx, ); } @@ -97,7 +104,7 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) { // istanbul ignore next (this.logger || context.cache.logger).warn( 'cache: Error fetching cached value for %s.%s(), args: %s!', - className, methodName, JSON.stringify(args), err + className, methodName, JSON.stringify(args), err, ); // istanbul ignore next @@ -106,6 +113,7 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) { }; } }; +// codebeat:enable[BLOCK_NESTING] cache.globalOptions = { adapter: RedisCache diff --git a/src/decorators/expose.ts b/src/decorators/expose.ts index a681f2b..44ab39e 100644 --- a/src/decorators/expose.ts +++ b/src/decorators/expose.ts @@ -1,37 +1,41 @@ /*! * IMQ-RPC Decorators: expose * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import 'reflect-metadata'; import * as acorn from 'acorn'; import { ArgDescription, ReturnValueDescription, - IMQRPCDescription + IMQRPCDescription, } from '..'; -require('acorn-es7-plugin')(acorn); - const TS_TYPES = [ 'object', 'string', 'number', 'boolean', 'null', - 'undefined' + 'undefined', ]; type CommentMetadata = { @@ -69,10 +73,10 @@ const RX_OPTIONAL = /^\[(.*?)\]$/; /** * Lookup and returns a list of argument names for a given function * - * @param {Function} fn + * @param {(...args: any[]) => any} fn * @return {string[]} */ -function argumentNames(fn: Function): string[] { +function argumentNames(fn: (...args: any[]) => any): string[] { let src: string = fn.toString(); return src.slice( src.indexOf('(') + 1, src.indexOf(')') @@ -95,7 +99,7 @@ function parseComment(src: string): CommentMetadata { returns: { description: '', type: '', - tsType: '' + tsType: '', } }; let match, tags = []; @@ -105,7 +109,7 @@ function parseComment(src: string): CommentMetadata { (cleanSrc.match(RX_DESCRIPTION) || [])[1] || '' ).trim(); - while (match = RX_TAG.exec(cleanSrc)) { + while ((match = RX_TAG.exec(cleanSrc))) { tags.push(match[1].trim()); } @@ -141,7 +145,7 @@ function parseComment(src: string): CommentMetadata { name, tsType, type, - isOptional + isOptional, }); break; } @@ -158,6 +162,7 @@ function parseComment(src: string): CommentMetadata { return data; } +// codebeat:disable[LOC,ABC] /** * Finds and parses methods and their comment blocks for a given class * @@ -167,16 +172,13 @@ function parseComment(src: string): CommentMetadata { function parseDescriptions(name: string, src: string) { const comments: acorn.Comment[] = []; const options: acorn.Options = { - ecmaVersion: 7, + ecmaVersion: 8, locations: true, ranges: true, allowReserved: true, onComment: comments, - plugins: { - asyncawait: true - } }; - const nodes = acorn.parse(src, options).body; + const nodes = (acorn.parse(src, options) as any).body; descriptions[name] = { inherits: 'Function' @@ -204,6 +206,9 @@ function parseDescriptions(name: string, src: string) { } } + const methods = node.body.body.filter((f: any) => + f.type === 'MethodDefinition'); + for (let method of node.body.body) { // istanbul ignore if if (method.type !== 'MethodDefinition') { @@ -219,7 +224,6 @@ function parseDescriptions(name: string, src: string) { } const methodStart: number = (method).start; - let commentEnd: number = blocks[0].end; let lastDif: number = methodStart - blocks[0].end; let foundBlock: acorn.Comment = blocks[0]; @@ -235,30 +239,28 @@ function parseDescriptions(name: string, src: string) { } lastDif = dif; - commentEnd = comment.end; foundBlock = comment; } - let expr = acorn.parseExpressionAt(src, commentEnd, options); - - if (expr.type === 'Identifier' && expr.name === 'async') { - expr = acorn.parseExpressionAt( - src, (expr).range[1], options - ); + if (!method.range || foundBlock.start > method.range[1]) { + continue; } - if ( - expr.type === 'CallExpression' && - (expr.callee).name === methodName - ) { + const index: number = methods.indexOf(method); + const prev: any = index && methods[index - 1]; + const prevBeforeComment = !prev + || (prev && prev.range && prev.range[1] <= foundBlock.start); + + if (prevBeforeComment) { // it's a method comment block!!!! descriptions[name][methodName] = { - comment: parseComment(foundBlock.value) + comment: parseComment(foundBlock.value), }; } } } } +// codebeat:enable[LOC,ABC] /** * Helper function to make easy descriptions parts extractions @@ -272,7 +274,7 @@ function parseDescriptions(name: string, src: string) { function get( prop: string, className: string, - method: string | symbol, + method: string, defaults: T ): T { if (descriptions[className] && descriptions[className][method]) { @@ -318,22 +320,24 @@ function cast(type: string) { * * @return {( * target: object, - * methodName: (string|symbol), - * descriptor: TypedPropertyDescriptor + * methodName: (string), + * descriptor: TypedPropertyDescriptor<(...args: any[]) => any> * ) => void} - decorator function */ -export function expose(): Function { - return function( +export function expose(): (...args: any[]) => any { + return function exposeDecorator( target: any, - methodName: string | symbol, - descriptor: TypedPropertyDescriptor + methodName: string, + descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) { let className: string = target.constructor.name; - let argNames: string[] = argumentNames(descriptor.value); + let argNames: string[] = argumentNames( + <(...args: any[]) => any>descriptor.value, + ); let retType = Reflect.getMetadata( 'design:returntype', target, - methodName + methodName, ); let retTypeName: string = retType ? retType.name : String(retType); @@ -347,7 +351,7 @@ export function expose(): Function { 'returns', className, methodName, { description: '', type: retTypeName, - tsType: cast(retTypeName) + tsType: cast(retTypeName), }); ret.type = retTypeName; @@ -359,7 +363,7 @@ export function expose(): Function { name: argNames[i], type: '', tsType: '', - isOptional : false + isOptional : false, }; } } @@ -367,7 +371,7 @@ export function expose(): Function { Reflect.getMetadata( 'design:paramtypes', target, - methodName + methodName, ).forEach((typeConstructor: any, i: number) => { args[i].type = args[i].type || typeConstructor.name; args[i].tsType = args[i].tsType || cast(args[i].type); @@ -376,12 +380,12 @@ export function expose(): Function { IMQRPCDescription.serviceDescription[className] = IMQRPCDescription.serviceDescription[className] || { inherits: descriptions[className].inherits, - methods: {} + methods: {}, }; IMQRPCDescription.serviceDescription[className].methods[methodName] = { description: get('description', className, methodName, ''), arguments: args, - returns: ret + returns: ret, }; } } diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 33c9b79..cb7772e 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -1,20 +1,30 @@ /*! - * Copyright (c) 2018, Mykhailo Stadnyk + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './expose'; export * from './remote'; export * from './lock'; export * from './cache'; export * from './property'; +export * from './indexed'; +export * from './logged'; diff --git a/src/decorators/indexed.ts b/src/decorators/indexed.ts new file mode 100644 index 0000000..5a00387 --- /dev/null +++ b/src/decorators/indexed.ts @@ -0,0 +1,72 @@ +/*! + * IMQ-RPC Decorators: property + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { IMQRPCDescription } from '..'; +import { Thunk } from '..'; + +/** + * Implements '@indexed' decorator factory + * This is used to specify complex service types which are need to expose + * types containing indexed definition, for example: + * + * @example + * ~~~typescript + * import { type } from '@imqueue/rpc'; + * + * @indexed('[fieldName: string]: any') + * class Schema { + * [fieldName: string]: any + * } + * ~~~ + * + * @return {(constructor: Function) => void} + */ +export function indexed(indexTypedef: string | Thunk): any { + return function (constructor: Function): any { + // istanbul ignore if + if (!indexTypedef) { + return ; // nothing to do here + } + + if (typeof indexTypedef === 'function') { + indexTypedef = indexTypedef(); + } + + // istanbul ignore if + if (typeof indexTypedef !== 'string') { + indexTypedef = String(indexTypedef); + } + + const typeName = constructor.name; + + IMQRPCDescription.typesDescription[typeName] = + IMQRPCDescription.typesDescription[typeName] || { + indexType: indexTypedef as string, + properties: {}, + inherits: Object.getPrototypeOf(constructor).name, + }; + + IMQRPCDescription.typesDescription[typeName] + .indexType = indexTypedef as string; + }; +} diff --git a/src/decorators/lock.ts b/src/decorators/lock.ts index eaaf779..8a56ed8 100644 --- a/src/decorators/lock.ts +++ b/src/decorators/lock.ts @@ -1,44 +1,60 @@ /*! * IMQ-RPC Decorators: lock * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { IMQLock, AcquiredLock, signature } from '..'; +export interface LockOptions { + disabled?: boolean; + skipArgs?: number[]; +} + /** * \@lock() decorator implementation * Will make all simultaneous similar method calls locked to be resolved with * the first obtained values. Similarity is identified by a bypassed method * argument values. * + * @param {boolean|LockOptions} enabledOrOptions - whether to enable locks or not * @return {( * target: any, - * methodName: (string|symbol), - * descriptor: TypedPropertyDescriptor + * methodName: (string), + * descriptor: TypedPropertyDescriptor<(...args: any[]) => any> * ) => void} */ -export function lock(enabled: boolean = true) { +export function lock(enabledOrOptions: boolean | LockOptions = true) { return function( target: any, methodName: string | symbol, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) { - const original = descriptor.value || - // istanbul ignore next - (() => {}); - const className = typeof target === 'function' + const enabled = typeof enabledOrOptions === 'boolean' + ? enabledOrOptions + : !enabledOrOptions.disabled; + const skipArgs = typeof enabledOrOptions === 'boolean' + ? undefined + : enabledOrOptions.skipArgs; + const original = descriptor.value; + const className: string = typeof target === 'function' ? target.name // static : target.constructor.name; // dynamic @@ -50,8 +66,18 @@ export function lock(enabled: boolean = true) { let sig: string = ''; if (withLocks) { - sig = signature(className, methodName, args); - lock = await IMQLock.acquire(sig); + sig = signature( + className, + methodName, + skipArgs ? args.filter((_, index) => + !~skipArgs.indexOf(index) + ) : args, + ); + lock = await IMQLock.acquire(sig, undefined, { + className, + methodName, + args, + }); if (!IMQLock.locked(sig)) { return lock; @@ -59,9 +85,14 @@ export function lock(enabled: boolean = true) { } try { - let result: any = original.apply(this, args); + // istanbul ignore next + let result: any = original + ? original.apply(this, args) + : undefined; - if (result && result.then) { + if (result && result.then && + typeof result.then === 'function' + ) { result = await result; } @@ -70,9 +101,7 @@ export function lock(enabled: boolean = true) { } return result; - } - - catch (err) { + } catch (err) { // istanbul ignore next if (withLocks) { IMQLock.release(sig, null, err); diff --git a/src/decorators/logged.ts b/src/decorators/logged.ts new file mode 100644 index 0000000..26f91de --- /dev/null +++ b/src/decorators/logged.ts @@ -0,0 +1,90 @@ +/*! + * IMQ-RPC Decorators: logged + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { ILogger } from '@imqueue/core'; + +export type LoggedLogLevel = 'info' | 'log' | 'warn' | 'error'; + +export interface LoggedDecoratorOptions { + level?: LoggedLogLevel; + logger?: ILogger; + doNotThrow?: boolean; +} + +// noinspection JSUnusedGlobalSymbols +/** + * Logger decorator factory for class methods. Will try, catch and log errors + * during method calls. If logger is bypassed, will use given logger, otherwise + * will try to use logger defined on class dynamically or statically or will + * fallback to console. + * + * @param {ILogger | LoggedDecoratorOptions} [options] - custom logger or + * logged decorator + * options + * @return {Function} - decorator function + */ +export function logged(options?: ILogger | LoggedDecoratorOptions) { + return ( + target: any, + methodName: string | symbol, + descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, + ) => { + const original = descriptor.value; + const level: LoggedLogLevel = ( + options && + (options as LoggedDecoratorOptions).level + ) + ? (options as LoggedDecoratorOptions).level as LoggedLogLevel + : 'error'; + const doThrow = !options || + !(options as LoggedDecoratorOptions).doNotThrow; + + descriptor.value = async function(...args: any[]): Promise { + try { + const ctx = (typeof this !== 'undefined' && this !== null) + ? this + : target; + + if (original) { + return await original.apply(ctx, args); + } + } catch (err) { + const logger: ILogger = (options && (options as LoggedDecoratorOptions).logger) + ? (options as LoggedDecoratorOptions).logger as ILogger + : (options && (options as any).error) + ? options as ILogger + : (this && (this as any).logger) + ? (this as any).logger as ILogger + : (target && (target as any).logger) + ? (target as any).logger as ILogger + : console; + + (logger as any)[level](err); + + if (doThrow) { + throw err; + } + } + }; + }; +} diff --git a/src/decorators/property.ts b/src/decorators/property.ts index a346acc..37a1c7a 100644 --- a/src/decorators/property.ts +++ b/src/decorators/property.ts @@ -1,29 +1,39 @@ /*! * IMQ-RPC Decorators: property * - * Copyright (c) 2018, Mykhailo Stadnyk - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { IMQRPCDescription } from '..'; +export interface Thunk { + (): any; +} + /** * Implements '@property' decorator factory * This is used to specify complex service types to be exposed * * @example * ~~~typescript - * import { property, expose, IMQService } from 'imq-rpc'; + * import { property, expose, IMQService } from '@imqueue/rpc'; * * class Address { * @property('string') @@ -73,19 +83,58 @@ import { IMQRPCDescription } from '..'; * * @return {( * target: any, - * methodName: (string|symbol), - * descriptor: TypedPropertyDescriptor + * methodName: (string), + * descriptor: TypedPropertyDescriptor<(...args: any[]) => any> * ) => void} */ -export function property(type: string, isOptional: boolean = false): any { - return function (target: any, propertyKey: string): any { +export function property( + type: string | Thunk | any, + isOptional: boolean = false, +): any { + // istanbul ignore if + if (!type) { + return ; + } + + return function ( + target: any, + propertyKey: string, + ): any { const typeName = target.constructor.name; + let typeDef: any; + + if (typeof type === 'function' && !(type as Function).name) { + type = (type as () => any)(); + } + + typeDef = type; + + if (Array.isArray(typeDef)) { + typeDef = typeDef[0]; + } + + if (typeDef && typeof typeDef !== 'string') { + typeDef = typeDef.name; + } + + if (Array.isArray(type)) { + typeDef += '[]'; + } + + // istanbul ignore if + if (!typeDef) { + typeDef = String(type); + } IMQRPCDescription.typesDescription[typeName] = - IMQRPCDescription.typesDescription[typeName] || {}; - IMQRPCDescription.typesDescription[typeName][propertyKey] = { - type, - isOptional + IMQRPCDescription.typesDescription[typeName] || { + properties: {}, + inherits: Object.getPrototypeOf(target.constructor).name, + }; + + IMQRPCDescription.typesDescription[typeName].properties[propertyKey] = { + type: typeDef as string, + isOptional, }; }; } diff --git a/src/decorators/remote.ts b/src/decorators/remote.ts index cfc5a4b..8d75c1e 100644 --- a/src/decorators/remote.ts +++ b/src/decorators/remote.ts @@ -1,43 +1,48 @@ /*! * IMQ-RPC Decorators: remote * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ /** * Implements '@remote' decorator factory * * @return {( * target: any, - * methodName: (string|symbol), - * descriptor: TypedPropertyDescriptor + * methodName: (string), + * descriptor: TypedPropertyDescriptor<(...args: any[]) => any> * ) => void} */ export function remote() { return function( target: any, - methodName: string | symbol, - descriptor: TypedPropertyDescriptor + methodName: string, + descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) { - const original = descriptor.value || - // istanbul ignore next - (() => {}); + const original = descriptor.value; descriptor.value = function(...args: any[]) { args.push(methodName); - return original.apply(this, args); + // istanbul ignore next + return (original ? original.apply(this, args) : undefined); }; } } diff --git a/src/helpers/fs.ts b/src/helpers/fs.ts new file mode 100644 index 0000000..2fc1862 --- /dev/null +++ b/src/helpers/fs.ts @@ -0,0 +1,82 @@ +/*! + * IMQ-RPC helpers: fs + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import * as fs from 'fs'; + +/** + * Checks if file exists at given path + * + * @param {string} path + * @return {Promise} + */ +export function fileExists(path: string) { + return new Promise(resolve => fs.access(path, err => resolve(!err))); +} + +/** + * Async mkdir + * + * @param {string} path + * @return {Promise} + */ +export function mkdir(path: string) { + return new Promise((resolve, reject) => fs.mkdir( + path, + generalCallback.bind(null, resolve, reject))); +} + +/** + * Async writeFile + * + * @param {string} path + * @param {string} content + * @return {Promise} + */ +export function writeFile(path: string, content: string) { + return new Promise((resolve, reject) => + fs.writeFile( + path, + content, + { encoding: 'utf8' }, + generalCallback.bind(null, resolve, reject))); +} + +/** + * Constructs and return callback which will resolve promise using + * given resolver and reject'or + * + * @param {() => void} resolve + * @param {(err: Error) => void} reject + * @param {Error} err + */ +function generalCallback( + resolve: () => void, + reject: (err: Error) => void, + err?: Error +) { + if (err) { + reject(err); + } + + resolve(); +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 13048ac..1172057 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,18 +1,26 @@ /*! - * Copyright (c) 2018, Mykhailo Stadnyk * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './signature'; export * from './os-uuid'; export * from './pid'; +export * from './fs'; diff --git a/src/helpers/os-uuid.ts b/src/helpers/os-uuid.ts index 3d671f2..710c7f2 100644 --- a/src/helpers/os-uuid.ts +++ b/src/helpers/os-uuid.ts @@ -1,19 +1,25 @@ /*! * IMQ-RPC helpers: osUuid * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ const { machineIdSync } = require('node-machine-id'); diff --git a/src/helpers/pid.ts b/src/helpers/pid.ts index 8ef9235..f1e1d56 100644 --- a/src/helpers/pid.ts +++ b/src/helpers/pid.ts @@ -1,24 +1,36 @@ /*! * IMQ-RPC helpers: pid, forgetPid * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import * as path from 'path'; import * as fs from 'fs'; -import { ILogger } from 'imq'; +import { ILogger } from '@imqueue/core'; +export const SIGNALS: string[] = [ + 'SIGTERM', + 'SIGINT', + 'SIGHUP', + 'SIGQUIT', +]; export const IMQ_TMP_DIR = process.env.TMPDIR || // istanbul ignore next '/tmp'; @@ -37,7 +49,11 @@ export function pid( path: string = IMQ_PID_DIR ): number { const pidFile = `${path}/${name}`; - const pidOpts = { encoding: 'utf8', flag: 'wx' }; + const pidOpts: { + encoding: string; + mode?: string | number | undefined; + flag?: string | undefined; + } = { encoding: 'utf8', flag: 'wx' }; if (!fs.existsSync(path)) { fs.mkdirSync(path); @@ -48,7 +64,11 @@ export function pid( while (!done) { try { - fs.writeFileSync(`${pidFile}-${id}.pid`, process.pid, pidOpts); + fs.writeFileSync( + `${pidFile}-${id}.pid`, + process.pid + '', + pidOpts as any, + ); done = true; } diff --git a/src/helpers/signature.ts b/src/helpers/signature.ts index d6eff05..b03977a 100644 --- a/src/helpers/signature.ts +++ b/src/helpers/signature.ts @@ -1,21 +1,27 @@ /*! * IMQ-RPC helpers: signature * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -const hash = require('murmurhash-native'); +import { fingerprint64 } from 'farmhash'; /** * Constructs and returns hash string for a given set of className, methodName @@ -31,7 +37,7 @@ export function signature( methodName: string | symbol, args: any[] ): string { - return hash.murmurHash128x64(JSON.stringify([ - className, methodName, args - ])); + const data = JSON.stringify([className, methodName, args]); + const hashBigInt = fingerprint64(data); + return hashBigInt.toString(16); } diff --git a/src/index.ts b/src/index.ts index 79147ce..a5f9f59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,23 @@ /*! - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './decorators'; export * from './helpers'; @@ -26,3 +32,5 @@ export * from './IMQRPCResponse'; export * from './IMQLock'; export * from './IMQService'; export * from './IMQClient'; +export * from './IMQMetadata'; +export * from './IMQRequestContext'; diff --git a/test/IMQCache.ts b/test/IMQCache.ts index bb4c9f4..1d68c4b 100644 --- a/test/IMQCache.ts +++ b/test/IMQCache.ts @@ -1,19 +1,25 @@ /*! * IMQCache Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { logger } from './mocks'; import { expect } from 'chai'; @@ -53,7 +59,6 @@ describe('IMQCache', () => { expect(IMQCache.adapters['RedisCache']) .to.be.instanceOf(RedisCache); - }); }); diff --git a/test/IMQClient.console.logger.spec.ts b/test/IMQClient.console.logger.spec.ts new file mode 100644 index 0000000..b26343f --- /dev/null +++ b/test/IMQClient.console.logger.spec.ts @@ -0,0 +1,134 @@ +/*! + * IMQClient console logger fallback branches coverage + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..'; + +class ConsoleClient extends IMQClient { + @remote() + public async ok(name?: string, meta?: IMQMetadata, delay?: IMQDelay) { + return this.remoteCall(...arguments); + } + @remote() + public async boom() { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient console logger fallbacks', () => { + let client: ConsoleClient; + + afterEach(async () => { + try { await client?.destroy(); } catch { /* ignore */ } + sinon.restore(); + }); + + it('should use console logger when BEFORE hook fails', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); } }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'C1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'good' })); + return id; + }); + + const res = await client.ok('x'); + expect(res).to.equal('good'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should use console logger when AFTER hook fails (resolve and reject paths)', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); } }); + await client.start(); + + const imq: any = (client as any).imq; + const send = sinon.stub(imq, 'send'); + + // success path + send.onFirstCall().callsFake(async (_to: string, request: any) => { + const id = 'C2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'S' })); + return id; + }); + + // reject path + send.onSecondCall().callsFake(async (_to: string, request: any) => { + const id = 'C3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') })); + return id; + }); + + const ok = await client.ok('ok'); + expect(ok).to.equal('S'); + + try { await client.boom(); } catch { /* expected */ } + + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); + + it('should use right-hand console branch in remoteCall when BEFORE hook fails', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + // Explicitly override default logger to be undefined to force `|| console` take the right branch + client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' })); + return id; + }); + + const res = await client.ok('x'); + expect(res).to.equal('ok'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should use right-hand console branch in imqCallResolver when AFTER hook fails on resolve', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'S' })); + return id; + }); + + const ok = await client.ok('ok'); + expect(ok).to.equal('S'); + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); + + it('should use right-hand console branch in imqCallRejector when AFTER hook fails on reject', async () => { + const warn = sinon.stub(console, 'warn' as any).callsFake(() => {}); + client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any }); + await client.start(); + + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => { + const id = 'MB3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') })); + return id; + }); + + try { await client.boom(); } catch { /* expected */ } + + expect(warn.callCount).to.be.greaterThan(0); + const messages = warn.getCalls().map(c => String(c.args[0])).join(' '); + expect(messages).to.contain(AFTER_HOOK_ERROR); + }); +}); diff --git a/test/IMQClient.extra.spec.ts b/test/IMQClient.extra.spec.ts new file mode 100644 index 0000000..fe57e81 --- /dev/null +++ b/test/IMQClient.extra.spec.ts @@ -0,0 +1,101 @@ +/*! + * IMQClient Extra Unit Tests (non-RPC, using send stubs) + */ +import './mocks'; +import { logger } from './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..'; + +class ExtraClient extends IMQClient { + @remote() + public async greet(name?: string, imqMetadata?: IMQMetadata, imqDelay?: IMQDelay) { + return this.remoteCall(...arguments); + } + @remote() + public async fail(imqDelay?: IMQDelay) { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient (extra branches without service)', () => { + let client: ExtraClient; + + afterEach(async () => { + await client?.destroy(); + sinon.restore(); + }); + + it('should warn on BEFORE_HOOK_ERROR and continue call', async function() { + this.timeout(5000); + const warn = sinon.stub(logger, 'warn'); + client = new ExtraClient({ logger, beforeCall: async () => { throw new Error('before'); } }); + await client.start(); + const imq: any = (client as any).imq; + sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => { + const id = 'ID1'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' })); + return id; + }); + const res = await client.greet('imq'); + expect(res).to.equal('ok'); + expect(warn.called).to.equal(true); + expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR); + }); + + it('should warn on AFTER_HOOK_ERROR for resolve and reject paths', async () => { + const warn = sinon.stub(logger, 'warn'); + client = new ExtraClient({ logger, afterCall: async () => { throw new Error('after'); } }); + await client.start(); + const imq: any = (client as any).imq; + const send = sinon.stub(imq, 'send'); + // success path + send.onFirstCall().callsFake(async (to: string, request: any) => { + const id = 'ID2'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'success' })); + return id; + }); + // reject path + send.onSecondCall().callsFake(async (to: string, request: any) => { + const id = 'ID3'; + setImmediate(() => imq.emit('message', { to: id, request, error: new Error('boom') })); + return id; + }); + const ok = await client.greet('ok'); + expect(ok).to.equal('success'); + try { await client.fail(); } catch (e) { /* expected */ } + // both paths should warn due to afterCall throwing + expect(warn.callCount).to.be.greaterThan(0); + expect(warn.getCalls().map(c => String(c.args[0])).join(' ')).to.contain(AFTER_HOOK_ERROR); + }); + + it('should emit event when resolver is missing', async () => { + client = new ExtraClient({ logger }); + await client.start(); + const evt = sinon.spy(); + client.on('greet', evt); + (client as any).imq.emit('message', { + to: 'unknown-id', + request: { method: 'greet' }, + data: { foo: 'bar' }, + }); + expect(evt.calledOnce).to.equal(true); + }); + + it('should sanitize invalid IMQDelay and pass IMQMetadata through request', async () => { + client = new ExtraClient({ logger }); + await client.start(); + const imq: any = (client as any).imq; + const sendStub = sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => { + expect(delay).to.equal(0); + expect(request.metadata).to.be.instanceOf(IMQMetadata); + const id = 'ID4'; + setImmediate(() => imq.emit('message', { to: id, request, data: 'x' })); + return id; + }); + const meta = new IMQMetadata({ a: 1 } as any); + const res = await client.greet('z', meta as any, new IMQDelay(-100) as any); + expect(res).to.equal('x'); + expect(sendStub.calledOnce).to.equal(true); + }); +}); diff --git a/test/IMQClient.generator.trailing.args.spec.ts b/test/IMQClient.generator.trailing.args.spec.ts new file mode 100644 index 0000000..1bb516c --- /dev/null +++ b/test/IMQClient.generator.trailing.args.spec.ts @@ -0,0 +1,80 @@ +/*! + * IMQClient generator trailing args removal coverage test + */ +import * as fs from 'fs'; +import { expect } from 'chai'; +import { IMQService, IMQClient, IMQDelay, IMQMetadata, expose, remote } from '..'; + +const CLIENTS_PATH = './test/clients-generator-trailing'; + +class GenTrailingService extends IMQService { + @expose() + public greet(name: string, meta?: IMQMetadata, delay?: IMQDelay) { + return `hi ${name}`; + } +} + +// We don't need a manual client; the generator will create it dynamically. + +describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', function () { + this.timeout(10000); + let service: GenTrailingService; + + function rmdirr(path: string) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach((file) => { + const curPath = `${path}/${file}`; + if (fs.lstatSync(curPath).isDirectory()) { + rmdirr(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } + } + + before(async () => { + service = new GenTrailingService(); + await service.start(); + }); + + after(async () => { + await service.destroy(); + rmdirr(CLIENTS_PATH); + }); + + it('should strip trailing metadata/delay from service description and add imqMetadata/imqDelay once', async () => { + const mod: any = await IMQClient.create('GenTrailingService', { + path: CLIENTS_PATH, + compile: true, + write: true, + }); + + // Ensure client class exists and can be instantiated + const client = new mod.GenTrailingClient(); + await client.start(); + + // Read generated TypeScript to verify the signature + const tsPath = `${CLIENTS_PATH}/GenTrailingService.ts`; + expect(fs.existsSync(tsPath)).to.equal(true); + const src = fs.readFileSync(tsPath, 'utf8'); + + // The generated method should not keep original parameter names 'meta' or 'delay' + // as they are stripped and replaced by imqMetadata/imqDelay at the end once. + const signatureRe = /public\s+async\s+greet\(([^)]*)\)/; + const m = src.match(signatureRe); + expect(m, 'method signature not found in generated client').to.not.equal(null); + const signature = (m as RegExpMatchArray)[1]; + + // It should include imqMetadata?: IMQMetadata and imqDelay?: IMQDelay + expect(signature).to.match(/imqMetadata\?\s*:\s*IMQMetadata/); + expect(signature).to.match(/imqDelay\?\s*:\s*IMQDelay/); + + // Original 'delay' parameter could still be present due to service signature, + // but generator must add imq* params at the end; presence is verified above. + + // Cleanup + await client.destroy(); + }); +}); diff --git a/test/IMQClient.methods.spec.ts b/test/IMQClient.methods.spec.ts new file mode 100644 index 0000000..f02c286 --- /dev/null +++ b/test/IMQClient.methods.spec.ts @@ -0,0 +1,81 @@ +/*! + * IMQClient methods Unit Tests (subscribe/unsubscribe/broadcast + signals) + */ +import './mocks'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { IMQClient, remote } from '..'; +import { logger } from './mocks'; + +class MethodsClient extends IMQClient { + // ensure there is at least one remote method (not used directly here) + @remote() + public async ping(): Promise { + return this.remoteCall(...arguments); + } +} + +describe('IMQClient methods', () => { + let client: MethodsClient; + + afterEach(async () => { + try { await client?.destroy(); } catch { /* ignore */ } + sinon.restore(); + }); + + it('should delegate subscribe() to subscriptionImq with service name', async () => { + client = new MethodsClient({ logger }); + const subImq: any = (client as any).subscriptionImq; + const spy = sinon.stub(subImq, 'subscribe').resolves(); + const handler = sinon.spy(); + await client.subscribe(handler as any); + expect(spy.calledOnce).to.equal(true); + expect(spy.firstCall.args[0]).to.equal(client.serviceName); + expect(spy.firstCall.args[1]).to.equal(handler); + }); + + it('should delegate unsubscribe() to subscriptionImq', async () => { + client = new MethodsClient({ logger }); + const subImq: any = (client as any).subscriptionImq; + const spy = sinon.stub(subImq, 'unsubscribe').resolves(); + await client.unsubscribe(); + expect(spy.calledOnce).to.equal(true); + }); + + it('should delegate broadcast() to imq.publish with queueName', async () => { + client = new MethodsClient({ logger }); + const imq: any = (client as any).imq; + const spy = sinon.stub(imq, 'publish').resolves(); + const payload: any = { hello: 'world' }; + await client.broadcast(payload); + expect(spy.calledOnce).to.equal(true); + expect(spy.firstCall.args[0]).to.equal(payload); + expect(spy.firstCall.args[1]).to.equal(client.queueName); + }); + + it('should handle process signals by calling destroy and then process.exit(0)', async () => { + const callbacks: Array<() => any> = []; + const onStub = sinon.stub(process as any, 'on').callsFake((sig: any, cb: any) => { + callbacks.push(cb); + return process as any; + }); + const exitStub = sinon.stub(process as any, 'exit'); + const clock = sinon.useFakeTimers(); + + client = new MethodsClient({ logger }); + const destroyStub = sinon.stub(client, 'destroy').resolves(); + + // invoke the first registered signal handler (e.g., SIGTERM) + await callbacks[0](); + // fast-forward shutdown timeout + clock.tick(10000); // IMQ_SHUTDOWN_TIMEOUT default + + expect(destroyStub.calledOnce).to.equal(true); + expect(exitStub.called).to.equal(true); + expect(exitStub.firstCall.args[0]).to.equal(0); + + clock.restore(); + onStub.restore(); + exitStub.restore(); + }); +}); diff --git a/test/IMQClient.ts b/test/IMQClient.ts index 0dc6bd1..ad4bba4 100644 --- a/test/IMQClient.ts +++ b/test/IMQClient.ts @@ -1,19 +1,25 @@ /*! * IMQClient Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import * as mock from 'mock-require'; import { logger } from './mocks'; @@ -24,13 +30,13 @@ import { IMQDelay, Description, expose, - remote + remote, } from '..'; import * as sinon from 'sinon'; import * as fs from 'fs'; import * as imqRpc from '..'; -mock('imq-rpc', imqRpc); +mock('@imqueue/rpc', imqRpc); class TestService extends IMQService { @@ -94,10 +100,14 @@ describe('IMQClient', () => { let client: TestServiceClient; before(async () => { - service = new TestService({ logger }); - await service.start(); - client = new TestServiceClient({ logger }); - await client.start(); + try { + service = new TestService({ logger }); + await service.start(); + client = new TestServiceClient({ logger }); + await client.start(); + } catch (err) { + console.error(err); + } }); after(async () => { @@ -129,6 +139,24 @@ describe('IMQClient', () => { expect(new TestServiceClient({}, undefined, 'TestClient').name) .to.contain('TestClient'); }); + + it('should re-use existing redis connection if singleQueue option ' + + 'enabled', () => { + const options = { logger, singleQueue: true }; + const client1: any = new TestServiceClient(options); + const client2: any = new TestServiceClient(options); + + expect(client1.imq).to.equal(client2.imq); + }); + + it('should not re-use existing redis connection if singleQueue option ' + + 'disabled', () => { + const options = { logger, singleQueue: false }; + const client1: any = new TestServiceClient(options); + const client2: any = new TestServiceClient(options); + + expect(client1.imq).to.not.equal(client2.imq); + }); }); describe('describe()', () => { @@ -172,13 +200,17 @@ describe('IMQClient', () => { try { const testService: any = await IMQClient.create( 'TestService', { logger, path: CLIENTS_PATH }); + // noinspection TypeScriptUnresolvedFunction const cli = new testService.TestClient({ logger }); expect(cli).instanceOf(IMQClient); const notExists = await new Promise(resolve => fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve)); - expect(!notExists).to.be.equal(true, 'TestService.ts does not exit'); + expect(!notExists).to.be.equal( + true, + 'TestService.ts does not exit' + ); cli.destroy(); rmdirr(CLIENTS_PATH); @@ -195,6 +227,7 @@ describe('IMQClient', () => { path: CLIENTS_PATH, write: false, }); + // noinspection TypeScriptUnresolvedFunction const cli = new testService.TestClient({ logger }); expect(cli).instanceOf(IMQClient); @@ -226,7 +259,7 @@ describe('IMQClient', () => { describe('stop()', () => { it('should stop client form serving messages', async () => { - const spy = sinon.spy((client).imq, 'stop'); + const spy = sinon.spy((client).imq, 'stop' as any); await client.stop(); expect(spy.called).to.be.true; spy.restore(); diff --git a/test/IMQDelay.ts b/test/IMQDelay.ts index ec34e9a..ba8afbc 100644 --- a/test/IMQDelay.ts +++ b/test/IMQDelay.ts @@ -1,19 +1,25 @@ /*! * IMQDelay Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import './mocks'; import { expect } from 'chai'; diff --git a/test/IMQLock.stringify.metadata.spec.ts b/test/IMQLock.stringify.metadata.spec.ts new file mode 100644 index 0000000..4cb6469 --- /dev/null +++ b/test/IMQLock.stringify.metadata.spec.ts @@ -0,0 +1,37 @@ +/*! + * IMQLock stringify(metadata) failure branch coverage test + */ +import { expect } from 'chai'; +import { IMQLock } from '..'; + +describe('IMQLock acquire() timeout with unstringifiable metadata', () => { + const KEY = 'circular-metadata-key'; + let originalTimeout: number; + + beforeEach(() => { + originalTimeout = IMQLock.deadlockTimeout; + IMQLock.deadlockTimeout = 10; // keep test fast + }); + + afterEach(() => { + IMQLock.deadlockTimeout = originalTimeout; + }); + + it('should reject with error containing "Unable to stringify metadata"', async () => { + // Acquire and hold the lock + const first = await IMQLock.acquire(KEY); + expect(first).to.equal(true); + + // Prepare circular metadata that will make JSON.stringify throw + const meta: any = { className: 'X', methodName: 'y', args: [] as any[] }; + (meta as any).self = meta; // circular reference + + try { + await IMQLock.acquire(KEY, undefined as any, meta); + expect.fail('should have been rejected by timeout'); + } catch (err: any) { + expect(err).to.be.instanceOf(Error); + expect(String(err.message)).to.contain('Unable to stringify metadata'); + } + }); +}); diff --git a/test/IMQLock.ts b/test/IMQLock.ts index 9ee6090..9f15f67 100644 --- a/test/IMQLock.ts +++ b/test/IMQLock.ts @@ -1,19 +1,25 @@ /*! * IMQLock Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import './mocks'; import { expect } from 'chai'; @@ -47,10 +53,10 @@ async function deadLocked() { } describe('IMQLock', () => { - this.timeout = 30000; + (this as any).timeout = 30000; - before(() => IMQLock.deadlockTimeout = LOCK_TIMEOUT); - after(() => IMQLock.deadlockTimeout = ORIGINAL_LOCK_TIMEOUT); + before(() => { IMQLock.deadlockTimeout = LOCK_TIMEOUT }); + after(() => { IMQLock.deadlockTimeout = ORIGINAL_LOCK_TIMEOUT }); it('should be a class', () => { expect(typeof IMQLock).to.equal('function'); diff --git a/test/IMQMetadata.ts b/test/IMQMetadata.ts new file mode 100644 index 0000000..cfa1bba --- /dev/null +++ b/test/IMQMetadata.ts @@ -0,0 +1,44 @@ +/*! + * IMQMetadata Unit Tests + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import './mocks'; +import { expect } from 'chai'; +import { IMQMetadata } from '..'; + +describe('IMQMetadata', () => { + it('should be a class', () => { + expect(typeof IMQMetadata).to.equal('function'); + }); + + it('should copy provided metadata props to instance index', () => { + const data = { a: 1, b: 'x', c: { y: true } } as any; + const m = new IMQMetadata(data); + // direct property access + expect((m as any).a).to.equal(1); + expect((m as any).b).to.equal('x'); + expect((m as any).c).to.deep.equal({ y: true }); + // index signature behavior + const keys = Object.keys(m); + expect(keys.sort()).to.deep.equal(['a','b','c']); + }); +}); diff --git a/test/IMQService.afterCall.spec.ts b/test/IMQService.afterCall.spec.ts new file mode 100644 index 0000000..9e81246 --- /dev/null +++ b/test/IMQService.afterCall.spec.ts @@ -0,0 +1,135 @@ +/*! + * IMQService afterCall and promise branch Unit Tests (incremental) + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { logger } from './mocks'; +import { + IMQService, + IMQRPCRequest, + expose, + AFTER_HOOK_ERROR, +} from '..'; + +class AfterHookService extends IMQService { + @expose() + public ping() { + return 'pong'; + } + + @expose() + public async asyncHello(name: string) { + return await new Promise(resolve => setTimeout(() => resolve(`Hello, ${name}!`), 5)); + } +} + +describe('IMQService hooks (afterCall) and promise branch', () => { + it('should execute afterCall hook without error', async () => { + const afterCall = sinon.spy(async () => {}); + const service: any = new AfterHookService({ logger, afterCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + const sendSpy = sinon.spy(service.imq, 'send'); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(sendSpy.called).to.be.true; + expect(afterCall.calledOnce).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + await service.destroy(); + }); + + it('should catch afterCall error and log AFTER_HOOK_ERROR', async () => { + const warnSpy = sinon.spy(logger, 'warn'); + const afterCall = async () => { throw new Error('after boom'); }; + const service: any = new AfterHookService({ logger, afterCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnSpy.called).to.be.true; + const hasAfter = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + warnSpy.restore(); + await service.destroy(); + }); + + it('should await promise-returning method result before sending', async () => { + const service: any = new AfterHookService({ logger }); + // make an exposed method return a Promise to hit thenable branch + service.exposed = async () => await new Promise(resolve => setTimeout(() => resolve('Hello, IMQ!'), 5)); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'exposed', + args: [], + }; + const id = uuid(); + + // stub to assert when send() is actually called, after promise resolved + const done = new Promise((resolve, reject) => { + sinon.stub(service.imq, 'send').callsFake(async (_to: string, response: any) => { + try { + expect(response.data).to.equal('Hello, IMQ!'); + resolve(); + } catch (err) { + reject(err); + } + return id; + }); + }); + + await service.start(); + service.imq.emit('message', request, id); + + await done; + + await service.destroy(); + }); +}); diff --git a/test/IMQService.console.logger.spec.ts b/test/IMQService.console.logger.spec.ts new file mode 100644 index 0000000..72058c8 --- /dev/null +++ b/test/IMQService.console.logger.spec.ts @@ -0,0 +1,74 @@ +/*! + * IMQService logger fallback coverage test + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..'; + +class ConsoleLoggerService extends IMQService { + @expose() + public ping() { return 'pong'; } +} + +describe('IMQService handleRequest logger fallback to console', () => { + let warnStub: sinon.SinonSpy; + + beforeEach(() => { + warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + }); + + afterEach(async () => { + sinon.restore(); + }); + + it('should use console when no custom logger provided and catch BEFORE hook error', async () => { + const beforeCall = async () => { throw new Error('before fails'); }; + const service: any = new ConsoleLoggerService({ beforeCall }); // no logger provided + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + + // Spy send to ensure regular flow continues and send is called even without afterCall + const sendSpy = sinon.spy(service.imq, 'send'); + + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(hasBefore).to.equal(true); + expect(sendSpy.called).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); + + it('should use console when afterCall throws (send() logger fallback)', async () => { + const afterCall = async () => { throw new Error('after fails'); }; + const service: any = new ConsoleLoggerService({ afterCall }); // no logger provided + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); +}); diff --git a/test/IMQService.hooks.spec.ts b/test/IMQService.hooks.spec.ts new file mode 100644 index 0000000..f3e0b09 --- /dev/null +++ b/test/IMQService.hooks.spec.ts @@ -0,0 +1,99 @@ +/*! + * IMQService Hooks Unit Tests (incremental) + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { logger } from './mocks'; +import { + IMQService, + IMQRPCRequest, + expose, + BEFORE_HOOK_ERROR, +} from '..'; + +class HookTestService extends IMQService { + @expose() + public ping() { + return 'pong'; + } +} + +describe('IMQService hooks (beforeCall)', () => { + it('should execute beforeCall hook without error', async () => { + const beforeCall = sinon.spy(async () => {}); + const service: any = new HookTestService({ logger, beforeCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + const sendSpy = sinon.spy(service.imq, 'send'); + + await service.start(); + service.imq.emit('message', request, id); + + // wait for async send + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(beforeCall.calledOnce).to.be.true; + expect(sendSpy.called).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + await service.destroy(); + }); + + it('should catch beforeCall error and log BEFORE_HOOK_ERROR', async () => { + const warnSpy = sinon.spy(logger, 'warn'); + const beforeCall = async () => { throw new Error('boom'); }; + const service: any = new HookTestService({ logger, beforeCall }); + const request: IMQRPCRequest = { + from: 'HookClient', + method: 'ping', + args: [], + }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnSpy.called).to.be.true; + const calledWithBefore = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(calledWithBefore).to.be.true; + resolve(undefined); + } catch (err) { + reject(err); + } + })); + + warnSpy.restore(); + await service.destroy(); + }); +}); diff --git a/test/IMQService.logger.fallback.spec.ts b/test/IMQService.logger.fallback.spec.ts new file mode 100644 index 0000000..558aab5 --- /dev/null +++ b/test/IMQService.logger.fallback.spec.ts @@ -0,0 +1,64 @@ +/*! + * IMQService logger fallback branches coverage tests + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..'; + +class FallbackLoggerService extends IMQService { + @expose() + public ping() { return 'pong'; } +} + +describe('IMQService logger fallback branches', () => { + beforeEach(() => sinon.restore()); + + it('should use console (fallback) inside handleRequest when beforeCall throws and options.logger is undefined', async () => { + const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + const beforeCall = async () => { throw new Error('boom'); }; + // Explicitly override default logger to undefined to force fallback branch + const service: any = new FallbackLoggerService({ beforeCall, logger: undefined as any }); + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR); + expect(hasBefore).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); + + it('should use console (fallback) inside send() when afterCall throws and options.logger is undefined', async () => { + const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {}); + const afterCall = async () => { throw new Error('after fails'); }; + // Explicitly override default logger to undefined to force fallback branch + const service: any = new FallbackLoggerService({ afterCall, logger: undefined as any }); + + const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] }; + const id = uuid(); + + await service.start(); + service.imq.emit('message', request, id); + + await new Promise((resolve, reject) => setTimeout(() => { + try { + expect(warnStub.called).to.equal(true); + const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR); + expect(hasAfter).to.equal(true); + resolve(undefined); + } catch (e) { reject(e); } + }, 1)); + + await service.destroy(); + }); +}); diff --git a/test/IMQService.signal.publish.spec.ts b/test/IMQService.signal.publish.spec.ts new file mode 100644 index 0000000..79c0ab4 --- /dev/null +++ b/test/IMQService.signal.publish.spec.ts @@ -0,0 +1,56 @@ +/*! + * IMQService signal handler and publish() coverage tests + */ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { uuid } from '@imqueue/core'; +import { IMQService, IMQRPCRequest, expose } from '..'; + +class SignalTestService extends IMQService { + @expose() + public ping() { + return 'pong'; + } +} + +describe('IMQService signal handler and publish()', () => { + afterEach(async () => { + sinon.restore(); + }); + + it('should call destroy() and log error on process signal (without exiting)', async () => { + const onStub = sinon.stub(process as any, 'on'); + const handlers: { sig: string; fn: (...args: any[]) => any }[] = []; + onStub.callsFake((sig: string, fn: any) => { handlers.push({ sig, fn }); return process as any; }); + + // use fake timers to avoid triggering real process.exit from setTimeout + const clock = sinon.useFakeTimers(); + + const logger: any = { info: () => {}, warn: () => {}, error: sinon.spy() }; + const service: any = new SignalTestService({ logger }); + + // make destroy reject to hit catch(logger.error) + sinon.stub(service, 'destroy').callsFake(async () => { throw new Error('boom'); }); + + // simulate first registered signal handler + expect(handlers.length).to.be.greaterThan(0); + await handlers[0].fn(); + + // let microtasks settle + await Promise.resolve(); + + expect(logger.error.called).to.equal(true); + + clock.restore(); + }); + + it('publish() should delegate to imq.publish', async () => { + const logger: any = { info: () => {}, warn: () => {}, error: () => {} }; + const service: any = new SignalTestService({ logger }); + const stub = sinon.stub(service.imq, 'publish').resolves(undefined as any); + + await service.publish({ id: uuid() } as any); + + expect(stub.calledOnce).to.equal(true); + }); +}); diff --git a/test/IMQService.ts b/test/IMQService.ts index 28501f3..e460432 100644 --- a/test/IMQService.ts +++ b/test/IMQService.ts @@ -1,26 +1,33 @@ /*! * IMQService Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { logger } from './mocks'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import * as cluster from 'cluster'; import { IMQService, IMQRPCRequest, Description, expose } from '..'; -import { uuid } from 'imq'; +import { uuid } from '@imqueue/core'; + +const cluster: any = require('cluster'); class TestService extends IMQService { @@ -91,10 +98,10 @@ describe('IMQService', () => { }); let counter = 0; - const spy = sinon.stub(cluster, 'fork').callsFake(async () => { + const spy = sinon.stub(cluster, 'fork').callsFake((async () => { (cluster).isMaster = false; (++counter == 1) && await service.start(); - }); + }) as any); await service.start(); diff --git a/test/cache/RedisCache.errors.spec.ts b/test/cache/RedisCache.errors.spec.ts new file mode 100644 index 0000000..552b81e --- /dev/null +++ b/test/cache/RedisCache.errors.spec.ts @@ -0,0 +1,61 @@ +/*! + * RedisCache error branches: methods before init should throw + */ +import { expect } from 'chai'; +import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../..'; + +describe('cache/RedisCache errors when not initialized', () => { + beforeEach(() => { delete (RedisCache as any).redis; }); + + it('get() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.get('x'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('set() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.set('x', 1); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('del() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.del('x'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); + + it('purge() should throw if redis not initialized', async () => { + const cache = new RedisCache(); + let threw = false; + try { + await cache.purge('mask'); + } catch (e: any) { + threw = true; + expect(e).to.be.instanceOf(TypeError); + expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR); + } + expect(threw).to.equal(true); + }); +}); diff --git a/test/cache/RedisCache.purge.promise.spec.ts b/test/cache/RedisCache.purge.promise.spec.ts new file mode 100644 index 0000000..9717723 --- /dev/null +++ b/test/cache/RedisCache.purge.promise.spec.ts @@ -0,0 +1,116 @@ +/*! + * RedisCache additional coverage: purge() and set() with Promise + */ +import { expect } from 'chai'; +import { RedisCache } from '../..'; + +class FakeRedis { + // minimal in-memory store + private store = new Map(); + public status: string = 'ready'; + public connected: boolean = true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public on(event: string, listener: (...args: any[]) => void): any { return this } + public once?: any; + public emit?: any; + public removeAllListeners(): void {} + public disconnect(_reconnect: boolean): void {} + public quit(): void {} + public end?(): void {} + + public async set(key: string, val: string, ..._args: any[]): Promise { + this.store.set(key, val); + return true; + } + public async get(key: string, ..._args: any[]): Promise { + return this.store.get(key) as any; + } + public async del(key: string): Promise { + const existed = this.store.delete(key); + return existed ? 1 : 0; + } + public async eval(script: string, _numkeys: number, ..._args: any[]): Promise { + // Extract mask from: redis.call('keys','') + const m = script.match(/redis\.call\('keys','([^']*)'\)/); + const mask = m ? m[1] : ''; + if (mask.endsWith('*')) { + const prefix = mask.slice(0, -1); + for (const key of Array.from(this.store.keys())) { + if (key.startsWith(prefix)) { + this.store.delete(key); + } + } + } else if (mask) { + this.store.delete(mask); + } + return true; + } +} + +class ErrorEvalRedis extends FakeRedis { + public async eval(): Promise { + throw new Error('eval failed'); + } +} + +describe('cache/RedisCache purge() and set() with Promise', () => { + afterEach(async () => { + await RedisCache.destroy(); + }); + + it('purge() should delete keys by wildcard and return true', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + // prepare some keys + await cache.set('keep', 'x'); // fully qualified: cov-cache:RedisCache:keep + await cache.set('del1', 1); + await cache.set('del2', 2); + + // unrelated key stored directly in connection should not be removed + await conn.set('other:namespace:key', 'y'); + + const mask = `${cache.options.prefix}:${cache.name}:del*`; + const ok = await cache.purge(mask); + expect(ok).to.equal(true); + + // del* gone, keep remains, and unrelated stays + expect(await cache.get('del1')).to.equal(undefined); + expect(await cache.get('del2')).to.equal(undefined); + expect(await cache.get('keep')).to.equal('x'); + // Sanity: unrelated + expect(await (conn as any).get('other:namespace:key')).to.equal('y'); + }); + + it('purge() should return false and log when eval throws', async () => { + const conn = new ErrorEvalRedis(); + const cache = new RedisCache(); + const logs: any[] = []; + const logger = { info: () => {}, error: (e: any) => logs.push(e) } as any; + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger }); + const res = await cache.purge('cov-cache:RedisCache:*'); + expect(res).to.equal(false); + expect(logs.length).to.be.greaterThan(0); + }); + + it('set() should accept Promise value and store resolved value', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + const key = 'promised'; + const valuePromise = new Promise((resolve) => setTimeout(() => resolve(42), 1)); + await cache.set(key, valuePromise); + expect(await cache.get(key)).to.equal(42); + }); + + it('del() should return boolean false when key did not exist', async () => { + const conn = new FakeRedis(); + const cache = new RedisCache(); + await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any }); + + const res = await cache.del('missing'); + expect(res).to.equal(false); + }); +}); diff --git a/test/cache/RedisCache.ts b/test/cache/RedisCache.ts index 30ec25f..29cf68e 100644 --- a/test/cache/RedisCache.ts +++ b/test/cache/RedisCache.ts @@ -1,24 +1,30 @@ /*! * RedisCache Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ -import { redis, logger } from '../mocks'; +import { Redis, logger } from '../mocks'; import { expect } from 'chai'; import { RedisCache } from '../..'; -import { uuid } from 'imq'; +import { IRedisClient, uuid } from '@imqueue/core'; describe('cache/RedisCache', () => { it('should be a class', () => { @@ -55,7 +61,7 @@ describe('cache/RedisCache', () => { }); it('should use connection from options if passed', async () => { - const conn = redis.createClient(); + const conn = new Redis() as unknown as IRedisClient; const cache = new RedisCache(); await cache.init({ conn, logger }); @@ -74,7 +80,6 @@ describe('cache/RedisCache', () => { after(async () => RedisCache.destroy()); - it('should return undefined if nothing found', async () => { expect(await cache.get(uuid())).to.be.undefined; }); diff --git a/test/decorators/cache.ts b/test/decorators/cache.ts index a3438a2..7ef5097 100644 --- a/test/decorators/cache.ts +++ b/test/decorators/cache.ts @@ -1,24 +1,30 @@ /*! * cache() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { logger } from '../mocks'; import { expect } from 'chai'; import { RedisCache, cache } from '../..'; -import { ILogger } from 'imq'; +import { ILogger } from '@imqueue/core'; class CacheTestClass { @@ -40,7 +46,7 @@ class CacheTestClass { describe('decorators/cache()', () => { let obj: any; - before(() => obj = new CacheTestClass()); + before(() => { obj = new CacheTestClass() }); after(async () => await RedisCache.destroy()); it('should be a function', () => { diff --git a/test/decorators/expose.ts b/test/decorators/expose.ts index aded394..4762c32 100644 --- a/test/decorators/expose.ts +++ b/test/decorators/expose.ts @@ -1,19 +1,25 @@ /*! * expose() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; @@ -53,6 +59,20 @@ class ExposeTestClass { // noinspection JSUnusedGlobalSymbols public nonExposedMethod() {} + /** + * @param {number} a + * @returns {string} + */ + @expose() + async withEmbeddedAsync(a: number) { + return await new Promise(async (resolve) => { + try { + resolve(a.toString()); + } catch(e) { + resolve(''); + } + }); + } } // noinspection JSUnusedLocalSymbols @@ -153,4 +173,12 @@ describe('decorators/expose()', () => { expect(description.ExposeTestClassExtended.inherits) .to.be.equal('ExposeTestClass'); }); -}); \ No newline at end of file + it('should parse embedded async functions', () => { + expect(description.ExposeTestClass.methods.withEmbeddedAsync.returns) + .to.deep.equal({ + description: '', + type: 'Promise', + tsType: 'string' + }); + }); +}); diff --git a/test/decorators/indexed.ts b/test/decorators/indexed.ts new file mode 100644 index 0000000..0bf5230 --- /dev/null +++ b/test/decorators/indexed.ts @@ -0,0 +1,68 @@ +/*! + * property() Function Unit Tests + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import '../mocks'; +import { expect } from 'chai'; +import { indexed, property, IMQRPCDescription } from '../..'; + +@indexed('[fieldName: string]: SchemaField') +class Schema { + [fieldName: string]: SchemaField; +} + +class SchemaField { + @property('string') + type: string; + + @property('any') + value: any; +} + +@indexed(() => `[fieldName: string]: ${SchemaField.name}`) +class SchemaThunk { + [fieldName: string]: SchemaField; +} + +const typesMetadata = IMQRPCDescription.typesDescription; + +describe('decorators/property()', () => { + it('should be a function', () => { + expect(typeof indexed).to.equal('function'); + }); + + it('should return decorator function', () => { + expect(typeof indexed('[name: string]: any')).to.equal('function'); + }); + + it('should properly fill exposed metadata', () => { + expect(typesMetadata.Schema).not.to.be.undefined; + expect(typesMetadata.Schema.indexType) + .contains('[fieldName: string]: SchemaField'); + }); + + it('should accept thunk definition', () => { + expect(typesMetadata.SchemaThunk).not.to.be.undefined; + expect(typesMetadata.SchemaThunk.indexType) + .contains('[fieldName: string]: SchemaField'); + }); +}); diff --git a/test/decorators/lock.skipArgs.spec.ts b/test/decorators/lock.skipArgs.spec.ts new file mode 100644 index 0000000..2e181eb --- /dev/null +++ b/test/decorators/lock.skipArgs.spec.ts @@ -0,0 +1,36 @@ +import '../mocks'; +import { expect } from 'chai'; +import { lock } from '../..'; + +class SkipArgsClass { + public calls: number = 0; + + @lock({ skipArgs: [1] }) + public async sum(a: number, b: number) { + this.calls++; + // delay to ensure overlapping calls and proper lock queuing + await new Promise(res => setTimeout(res, 20)); + return a + (b || 0); + } +} + +describe('decorators/lock() with skipArgs', () => { + beforeEach(() => { + // Ensure locks are enabled for this test regardless of other tests + delete process.env['DISABLE_LOCKS']; + }); + + it('should ignore specified args when building lock signature', async () => { + const obj = new SkipArgsClass(); + const results = await Promise.all([ + obj.sum(1, 10), + obj.sum(1, 20), + obj.sum(1, 30), + ]); + // Since b (index 1) is skipped in signature, all concurrent calls share one lock + const uniq = [...new Set(results)]; + expect(uniq.length).to.equal(1); + // Original method body should be executed only once + expect(obj.calls).to.equal(1); + }); +}); diff --git a/test/decorators/lock.ts b/test/decorators/lock.ts index 859ec4f..da5467e 100644 --- a/test/decorators/lock.ts +++ b/test/decorators/lock.ts @@ -1,19 +1,25 @@ /*! * lock() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; diff --git a/test/decorators/logged.targetLogger.spec.ts b/test/decorators/logged.targetLogger.spec.ts new file mode 100644 index 0000000..e490c1d --- /dev/null +++ b/test/decorators/logged.targetLogger.spec.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import { logged } from '../../src/decorators/logged'; + +// Small, targeted tests to hit missing branches in logged.ts (lines 64 and 72) +describe('decorators/logged() target logger and no-original branches', () => { + it('should return undefined when original is missing (no call, no throw)', async () => { + const descriptor: PropertyDescriptor = { value: undefined } as any; + const target: any = {}; + + // Apply decorator to a descriptor without original implementation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'missing', descriptor); + + // Call decorated function directly; should just return undefined + const result = await (descriptor.value as any)(); + expect(result).to.be.undefined; + }); + + it('should use target logger when this is undefined and rethrow by default', async () => { + const error = new Error('boom'); + const original = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + + const targetLoggerCalls: Error[] = []; + const target: any = { + logger: { + error: (e: Error) => { targetLoggerCalls.push(e); }, + warn: () => void 0, + info: () => void 0, + log: () => void 0, + }, + }; + + // Apply decorator; we will call without binding `this` so inside wrapper + // `this` will be undefined and the code should select target.logger (line 72) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'willThrow', descriptor); + + try { + // Call as a plain function to keep `this` undefined in strict mode + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(targetLoggerCalls).to.have.length(1); + expect(targetLoggerCalls[0]).to.equal(error); + } + }); + + it('should return undefined when original is missing (no call, no throw)', async () => { + const descriptor: PropertyDescriptor = { value: undefined } as any; + const target: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'missing', descriptor); + const result = await (descriptor.value as any)(); + expect(result).to.be.undefined; + }); + + it('should use target logger when this is undefined and rethrow by default', async () => { + const error = new Error('boom'); + const original = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + + const targetLoggerCalls: Error[] = []; + const target: any = { + logger: { + error: (e: Error) => { targetLoggerCalls.push(e); }, + warn: () => void 0, + info: () => void 0, + log: () => void 0, + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'willThrow', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(targetLoggerCalls).to.have.length(1); + expect(targetLoggerCalls[0]).to.equal(error); + } + }); +}); diff --git a/test/decorators/logged.thisLogger.spec.ts b/test/decorators/logged.thisLogger.spec.ts new file mode 100644 index 0000000..1bfb404 --- /dev/null +++ b/test/decorators/logged.thisLogger.spec.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { logged } from '../../src/decorators/logged'; + +/* + * Additional coverage for logged.ts to reach 100% branches: + * - Exercise `this || target` left-hand path (this is truthy) + * - Ensure `this.logger` is preferred when available + * - Cover options.logger usage and default rethrow behavior + * - Cover passing an ILogger directly as options + */ + +describe('decorators/logged() this logger and options logger branches', () => { + afterEach(() => { + // nothing to cleanup + }); + + it('should use bound `this` in apply(), prefer this.logger, not rethrow when doNotThrow=true', async () => { + const error = new Error('use-this'); + let capturedThis: any; + // original captures `this` then throws + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function original(this: any) { + capturedThis = this; + throw error; + }; + + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { logger: { error: () => void 0, warn: () => void 0, info: () => void 0, log: () => void 0 } }; + + // Apply decorator: do not throw and use non-default level to verify dynamic call + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged({ doNotThrow: true, level: 'warn' }) as any)(target, 'x', descriptor); + + const thisLoggerCalls: { level: string; error: Error }[] = []; + const self: any = { + logger: { + error: (_e: Error) => thisLoggerCalls.push({ level: 'error', error: _e }), + warn: (_e: Error) => thisLoggerCalls.push({ level: 'warn', error: _e }), + info: (_e: Error) => thisLoggerCalls.push({ level: 'info', error: _e }), + log: (_e: Error) => thisLoggerCalls.push({ level: 'log', error: _e }), + }, + }; + + // Call with `this` bound so (this || target) takes left-hand branch + const res = await (descriptor.value as any).call(self); + expect(res).to.be.undefined; // swallowed due to doNotThrow + expect(capturedThis).to.equal(self); + expect(thisLoggerCalls).to.have.length(1); + expect(thisLoggerCalls[0].level).to.equal('warn'); + expect(thisLoggerCalls[0].error).to.equal(error); + }); + + it('should use options.logger when provided and rethrow by default', async () => { + const error = new Error('use-options-logger'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + + const calls: Error[] = []; + const logger = { info: (e: Error) => { calls.push(e); }, error: () => {}, warn: () => {}, log: () => {} } as any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged({ logger, level: 'info' }) as any)(target, 'y', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(calls).to.have.length(1); + expect(calls[0]).to.equal(error); + } + }); + + it('should treat options itself as ILogger when it has error() method', async () => { + const error = new Error('options-is-logger'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = () => { throw error; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + + const calls: Error[] = []; + const optionsLogger = { error: (e: Error) => calls.push(e), log: () => {}, warn: () => {}, info: () => {} } as any; + + // Pass logger directly instead of options object + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged(optionsLogger) as any)(target, 'z', descriptor); + + try { + await (descriptor.value as any)(); + expect.fail('should have thrown'); + } catch (e) { + expect(e).to.equal(error); + expect(calls).to.have.length(1); + expect(calls[0]).to.equal(error); + } + }); + + it('should use bound `this` in apply() and return value when original resolves', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { return this.answer; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'ok', descriptor); + const self: any = { answer: 42 }; + const res = await (descriptor.value as any).call(self); + expect(res).to.equal(42); + }); + + it('should use target as `this` when caller `this` is falsy (right branch of this||target)', async () => { + let gotThis: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { gotThis = this; return this.mark; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { mark: 'TARGET' }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'right', descriptor); + // Explicitly call with undefined `this` + const res = await (descriptor.value as any).call(undefined); + expect(res).to.equal('TARGET'); + expect(gotThis).to.equal(target); + }); + + it('should use target when caller `this` is null (second condition false)', async () => { + let gotThis: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const original: any = function(this: any) { gotThis = this; return this.mark; }; + const descriptor: PropertyDescriptor = { value: original } as any; + const target: any = { mark: 'TARGET-NULL' }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (logged() as any)(target, 'right-null', descriptor); + // Explicitly call with null `this` to pass first check and fail second + const res = await (descriptor.value as any).call(null); + expect(res).to.equal('TARGET-NULL'); + expect(gotThis).to.equal(target); + }); +}); diff --git a/test/decorators/logged.ts b/test/decorators/logged.ts new file mode 100644 index 0000000..f5a8b54 --- /dev/null +++ b/test/decorators/logged.ts @@ -0,0 +1,141 @@ +import 'reflect-metadata'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { logged } from '../../src/decorators/logged'; + +describe('decorators/logged()', () => { + it('should be a function and return decorator function', () => { + expect(typeof logged).to.equal('function'); + // @ts-ignore + const decorator = logged(); + expect(typeof decorator).to.equal('function'); + }); + + it('should fallback to console logger and rethrow by default', async () => { + const error = new Error('boom'); + const stub = sinon.stub(console, 'error'); + class A { + // @ts-ignore + @logged() + public fail() { + throw error; + } + } + + try { + await new A().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(stub.calledOnce).to.equal(true); + expect(stub.firstCall.args[0]).to.equal(error); + } finally { + stub.restore(); + } + }); + + it('should use provided logger, level and suppress throw when doNotThrow', async () => { + const error = new Error('warned'); + const myLogger = { + warn: sinon.stub(), + error: sinon.stub(), + log: sinon.stub(), + info: sinon.stub(), + } as any; + + class B { + // @ts-ignore + @logged({ logger: myLogger, level: 'warn', doNotThrow: true }) + public fail() { + throw error; + } + } + + const res = await new B().fail(); + expect(res).to.equal(undefined); + expect(myLogger.warn.calledOnce).to.equal(true); + expect(myLogger.warn.firstCall.args[0]).to.equal(error); + }); + + it('should accept ILogger directly and rethrow by default', async () => { + const error = new Error('as-logger'); + const myLogger = { + warn: sinon.stub(), + error: sinon.stub(), + log: sinon.stub(), + info: sinon.stub(), + } as any; + + class C { + // @ts-ignore + @logged(myLogger) + public fail() { + throw error; + } + } + + try { + await new C().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(myLogger.error.calledOnce).to.equal(true); + expect(myLogger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should use instance logger when present and rethrow', async () => { + const error = new Error('inst-logger'); + const myLogger = { + error: sinon.stub(), + } as any; + class E { + public logger = myLogger; + // @ts-ignore + @logged() + public fail() { + throw error; + } + } + try { + await new E().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(myLogger.error.calledOnce).to.equal(true); + expect(myLogger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should use target logger when present on prototype for instance method', async () => { + const error = new Error('proto-logger'); + const protologger = { error: sinon.stub() } as any; + class F { + // @ts-ignore + @logged() + public fail() { throw error; } + } + (F.prototype as any).logger = protologger; + try { + await new F().fail(); + expect.fail('should throw'); + } catch (e) { + expect(e).to.equal(error); + expect(protologger.error.calledOnce).to.equal(true); + expect(protologger.error.firstCall.args[0]).to.equal(error); + } + }); + + it('should pass through successful return value', async () => { + class D { + // @ts-ignore + @logged() + public ok() { + return 42; + } + } + + const v = await new D().ok(); + expect(v).to.equal(42); + }); +}); diff --git a/test/decorators/property.ts b/test/decorators/property.ts index 0a40d7c..0ca4b50 100644 --- a/test/decorators/property.ts +++ b/test/decorators/property.ts @@ -1,19 +1,25 @@ /*! * property() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; @@ -45,6 +51,33 @@ class TestBaseComplexType { point: TestPointType; } +class TestExtendedComplexType extends TestPointType { + @property('boolean') + public is3d: boolean; +} + +class ThunkComplexTypeDef { + @property(TestPointType) + point: TestPointType; + + @property(() => ThunkComplexTypeDef) + self: ThunkComplexTypeDef; +} + +class ArrayPropTypes { + @property(() => Array.of(TestPointType)) + points: TestPointType[]; + + @property([TestPointType]) + otherPoints: TestPointType[]; + + @property(Array.of('TestPointType')) + morePoints: TestPointType[]; + + @property(() => ['TestPointType']) + yetMorePoints: TestPointType[]; +} + const typesMetadata = IMQRPCDescription.typesDescription; describe('decorators/property()', () => { @@ -59,44 +92,73 @@ describe('decorators/property()', () => { it('should properly fill exposed metadata', () => { expect(typesMetadata.TestBaseComplexType).not.to.be.undefined; expect(typesMetadata.TestPointType).not.to.be.undefined; + expect(typesMetadata.TestPointType.properties).not.to.be.undefined; - expect(typesMetadata.TestPointType.x).not.to.be.undefined; - expect(typesMetadata.TestPointType.y).not.to.be.undefined; - expect(typesMetadata.TestPointType.z).not.to.be.undefined; + expect(typesMetadata.TestPointType.properties.x).not.to.be.undefined; + expect(typesMetadata.TestPointType.properties.y).not.to.be.undefined; + expect(typesMetadata.TestPointType.properties.z).not.to.be.undefined; - expect(typesMetadata.TestPointType.x.type) + expect(typesMetadata.TestPointType.properties.x.type) .to.equal('number'); - expect(typesMetadata.TestPointType.y.type) + expect(typesMetadata.TestPointType.properties.y.type) .to.equal('number'); - expect(typesMetadata.TestPointType.z.type) + expect(typesMetadata.TestPointType.properties.z.type) .to.equal('number'); - expect(typesMetadata.TestPointType.x.isOptional) + expect(typesMetadata.TestPointType.properties.x.isOptional) .to.be.false; - expect(typesMetadata.TestPointType.y.isOptional) + expect(typesMetadata.TestPointType.properties.y.isOptional) .to.be.false; - expect(typesMetadata.TestPointType.z.isOptional) + expect(typesMetadata.TestPointType.properties.z.isOptional) .to.be.true; - expect(typesMetadata.TestBaseComplexType.mandatoryStr) + expect(typesMetadata.TestBaseComplexType.properties.mandatoryStr) .not.to.be.undefined; - expect(typesMetadata.TestBaseComplexType.nonMandatoryStr) + expect(typesMetadata.TestBaseComplexType.properties.nonMandatoryStr) .not.to.be.undefined; - expect(typesMetadata.TestBaseComplexType.point) + expect(typesMetadata.TestBaseComplexType.properties.point) .not.to.be.undefined; - expect(typesMetadata.TestBaseComplexType.mandatoryStr.type) + expect(typesMetadata.TestBaseComplexType.properties.mandatoryStr.type) .to.equal('string'); - expect(typesMetadata.TestBaseComplexType.nonMandatoryStr.type) - .to.equal('string'); - expect(typesMetadata.TestBaseComplexType.point.type) + expect( + typesMetadata.TestBaseComplexType.properties.nonMandatoryStr.type + ).to.equal('string'); + expect(typesMetadata.TestBaseComplexType.properties.point.type) .to.equal('TestPointType'); - expect(typesMetadata.TestBaseComplexType.mandatoryStr.isOptional) - .to.be.false; - expect(typesMetadata.TestBaseComplexType.nonMandatoryStr.isOptional) - .to.be.true; - expect(typesMetadata.TestBaseComplexType.point.isOptional) + expect( + typesMetadata.TestBaseComplexType.properties.mandatoryStr.isOptional + ).to.be.false; + expect( + typesMetadata.TestBaseComplexType.properties.nonMandatoryStr + .isOptional + ).to.be.true; + expect(typesMetadata.TestBaseComplexType.properties.point.isOptional) .to.be.false; + expect(typesMetadata.TestExtendedComplexType.inherits) + .equals('TestPointType'); + expect(typesMetadata.TestPointType.inherits) + .not.to.be.ok; + }); + + it('should allow objects and thunks as typedefs', () => { + expect(typesMetadata.ThunkComplexTypeDef).not.to.be.undefined; + expect(typesMetadata.ThunkComplexTypeDef.properties.point.type) + .equals('TestPointType'); + expect(typesMetadata.ThunkComplexTypeDef.properties.self.type) + .equals('ThunkComplexTypeDef'); + }); + + it('should accept array types', () => { + expect(typesMetadata.ArrayPropTypes).not.to.be.undefined; + expect(typesMetadata.ArrayPropTypes.properties.points.type) + .equals('TestPointType[]'); + expect(typesMetadata.ArrayPropTypes.properties.otherPoints.type) + .equals('TestPointType[]'); + expect(typesMetadata.ArrayPropTypes.properties.morePoints.type) + .equals('TestPointType[]'); + expect(typesMetadata.ArrayPropTypes.properties.yetMorePoints.type) + .equals('TestPointType[]'); }); -}); \ No newline at end of file +}); diff --git a/test/decorators/remote.ts b/test/decorators/remote.ts index 3af0aa9..3abe0b6 100644 --- a/test/decorators/remote.ts +++ b/test/decorators/remote.ts @@ -1,19 +1,25 @@ /*! * remote() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; diff --git a/test/helpers/fs.ts b/test/helpers/fs.ts new file mode 100644 index 0000000..3e3b1a9 --- /dev/null +++ b/test/helpers/fs.ts @@ -0,0 +1,92 @@ +/*! + * FS helpers Unit Tests + * + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. + */ +import * as mock from 'mock-require'; +import { expect } from 'chai'; + +const fsMock = { + access(path: string, cb: (err?: Error) => void) { + if (path === 'exist') { + cb(); + } + if (path === 'unexist') { + cb(new Error); + } + }, + mkdir(path: string, cb: (err?: | Error) => void) { + if (path === 'done') { + cb(); + } + if (path === 'error') { + cb(new Error); + } + }, + writeFile( + path: string, + content: string, + options: { [ key: string ]: string }, + cb: (err?: Error) => void + ) { + if (path === 'done') { + cb(); + } + if (path === 'error') { + cb(new Error); + } + }, +}; + +let fileExists: (path: string) => Promise; +let mkdir: (path: string) => Promise; +let writeFile: (path: string, content: string) => Promise; + +describe('fs helpers', () => { + before (function before() { + mock('fs', fsMock); + delete require.cache[require.resolve('../../src/helpers/fs')]; + const fs = require('../../src/helpers/fs'); + fileExists = fs.fileExists; + mkdir = fs.mkdir; + writeFile = fs.writeFile; + }); + after(() => { + mock.stop('fs'); + }); + + it('should check file existance', () => + fileExists('exist').then((status: boolean) => expect(status).to.be.true)); + + it('should check file unexistance', () => + fileExists('unexist').then((status: boolean) => expect(status).to.be.false)); + + it('should mkdir', () => mkdir('done')); + + it('should mkdir with error', () => + mkdir('error').catch((err: Error) => expect(err).to.be.an.instanceof(Error))); + + it('should write file', async () => writeFile('done', '')); + + it('should write file with error', () => + writeFile('error', '').catch((err: Error) => expect(err).to.be.an.instanceof(Error))); +}); + diff --git a/test/helpers/os-uuid.ts b/test/helpers/os-uuid.ts index 27c59d5..0d375c6 100644 --- a/test/helpers/os-uuid.ts +++ b/test/helpers/os-uuid.ts @@ -1,19 +1,25 @@ /*! * osUuid() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; diff --git a/test/helpers/pid.ts b/test/helpers/pid.ts index 746854f..b1e69ef 100644 --- a/test/helpers/pid.ts +++ b/test/helpers/pid.ts @@ -1,19 +1,25 @@ /*! * pid(), forgetPid() Functions Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import { logger } from '../mocks'; import { expect } from 'chai'; @@ -41,8 +47,8 @@ function rmdirr(path: string) { describe('helpers/pid()', () => { let TEST_PID_DIR: string; - beforeEach(() => TEST_PID_DIR = `${IMQ_TMP_DIR}/${uuid()}`); - afterEach(() => rmdirr(TEST_PID_DIR)); + beforeEach(() => { TEST_PID_DIR = `${IMQ_TMP_DIR}/${uuid()}` }); + afterEach(() => { rmdirr(TEST_PID_DIR) }); it('should be a function', () => { expect(typeof pid).to.equal('function'); @@ -70,8 +76,8 @@ describe('helpers/pid()', () => { describe('helpers/forgetPid()', () => { let TEST_PID_DIR: string; - beforeEach(() => TEST_PID_DIR = `${IMQ_TMP_DIR}/${uuid()}`); - afterEach(() => rmdirr(TEST_PID_DIR)); + beforeEach(() => { TEST_PID_DIR = `${IMQ_TMP_DIR}/${uuid()}` }); + afterEach(() => { rmdirr(TEST_PID_DIR) }); it('should be a function', () => { expect(typeof forgetPid).to.equal('function'); diff --git a/test/helpers/signature.ts b/test/helpers/signature.ts index 772cbb2..7b4b510 100644 --- a/test/helpers/signature.ts +++ b/test/helpers/signature.ts @@ -1,19 +1,25 @@ /*! * signature() Function Unit Tests * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import '../mocks'; import { expect } from 'chai'; @@ -25,7 +31,7 @@ describe('helpers/signature()', () => { }); it('should return hash string', () => { - expect(/^[0-9a-f]{32}$/.test(signature('A', 'a', []))).to.be.true; + expect(/^[0-9a-f]{16,32}$/.test(signature('A', 'a', []))).to.be.true; }); it('should return same hash string for the same arguments bypassed', () => { diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index b1c54d3..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,5 +0,0 @@ ---require ts-node/register ---require source-map-support/register ---recursive ---bail ---full-trace diff --git a/test/mocks/index.ts b/test/mocks/index.ts index c8bdbd0..723a20e 100644 --- a/test/mocks/index.ts +++ b/test/mocks/index.ts @@ -1,19 +1,25 @@ /*! * IMQ-RPC Unit Test Mocks Exports * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ export * from './logger'; export * from './redis'; diff --git a/test/mocks/logger.ts b/test/mocks/logger.ts index 7ceff45..6142c19 100644 --- a/test/mocks/logger.ts +++ b/test/mocks/logger.ts @@ -1,19 +1,25 @@ /*! * IMQ-RPC Unit Test Mocks: logger * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ // noinspection JSUnusedGlobalSymbols export const logger: any = { diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index d8f1a43..2fe90d5 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -1,19 +1,25 @@ /*! * IMQ Unit Test Mocks: redis * - * Copyright (c) 2018, Mykhailo Stadnyk + * I'm Queue Software Project + * Copyright (C) 2025 imqueue.com * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you want to use this code in a closed source (commercial) project, you can + * purchase a proprietary commercial license. Please contact us at + * to get commercial licensing options. */ import * as mock from 'mock-require'; import { EventEmitter } from 'events'; @@ -26,32 +32,43 @@ function sha1(str: string) { } /** - * @implements {IRedisClient} + * @implements {Redis} */ export class RedisClientMock extends EventEmitter { private static __queues__: any = {}; private static __clientList: any = {}; private __rt: any; private static __keys: any = {}; - private __name: string = ''; private static __scripts: any = {}; + private __name: string = ''; // noinspection JSUnusedGlobalSymbols public connected: boolean = true; - public ready: boolean = true; + public status = 'ready'; - constructor() { + constructor(options: any = {}) { super(); setTimeout(() => { this.emit('ready', this); }); + + if (options.connectionName) { + this.__name = options.connectionName; + RedisClientMock.__clientList[options.connectionName] = true; + } + } + + // noinspection JSUnusedGlobalSymbols + public async connect(): Promise { + this.connected = true; + this.status = 'ready'; } // noinspection JSUnusedGlobalSymbols public end() {} // noinspection JSUnusedGlobalSymbols - public quit() {} + public async quit(): Promise {} - // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic + // noinspection JSMethodCanBeStatic public async set(...args: any[]): Promise { let [key, val, units, expire, nx] = args; @@ -78,7 +95,6 @@ export class RedisClientMock extends EventEmitter { return true; } - // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic public async get(...args: any[]): Promise { const [key] = args; const val = RedisClientMock.__keys[key]; @@ -88,7 +104,7 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public async setnx(...args: any[]): Promise { + public setnx(...args: any[]): number { const self = RedisClientMock; const key = args.shift(); let result = 0; @@ -98,75 +114,67 @@ export class RedisClientMock extends EventEmitter { result = 1; } } - const cb = args.pop(); - typeof cb === 'function' && cb(null, result); - return true; - } - // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public async del(...args: any[]): Promise { - const self = RedisClientMock; - const cb = args.pop(); - let count = 0; - for (let key of args) { - if (self.__keys[key] !== undefined) { - delete self.__keys[key]; - count++; - } - if (self.__queues__[key] !== undefined) { - delete self.__queues__[key]; - count++; - } - } - typeof cb === 'function' && cb(null, count); - return true; + this.cbExecute(args.pop(), null, result); + + return result; } // noinspection TypescriptExplicitMemberType,JSMethodCanBeStatic - public lpush(key: string, value: any, cb?: any): boolean { + public lpush(key: string, value: any, cb?: any): number { const self = RedisClientMock; if (!self.__queues__[key]) { self.__queues__[key] = []; } self.__queues__[key].push(value); - cb(null, 1); - return true; + this.cbExecute(cb, null, 1); + return 1; } - public brpop(...args: any[]): boolean { + public async brpop(...args: any[]): Promise { const [key, timeout, cb] = args; const q = RedisClientMock.__queues__[key] || []; if (!q.length) { this.__rt && clearTimeout(this.__rt); - this.__rt = setTimeout(() => this.brpop( - key, timeout, cb - ), timeout || 100); + + return new Promise(resolve => { + this.__rt = setTimeout(() => resolve(this.brpop( + key, timeout, cb, + )), timeout || 100); + }); } else { - cb && cb(null, [key, q.shift()]); + const result = [key, q.shift()]; + + this.cbExecute(cb, null, [key, q.shift()]); + + return result; } - return true; } - public brpoplpush( + public async brpoplpush( from: string, to: string, timeout: number, cb?: Function - ): boolean { + ): Promise { const fromQ = RedisClientMock.__queues__[from] = RedisClientMock.__queues__[from] || []; const toQ = RedisClientMock.__queues__[to] = RedisClientMock.__queues__[to] || []; if (!fromQ.length) { this.__rt && clearTimeout(this.__rt); - this.__rt = setTimeout(() => this.brpoplpush( - from, to, timeout, cb - ), timeout || 100); + + return new Promise(resolve => { + this.__rt = setTimeout(() => resolve(this.brpoplpush( + from, to, timeout, cb, + )), timeout || 100); + }); } else { toQ.push(fromQ.shift()); cb && cb(null, '1'); + + return '1'; } - return true; } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -174,16 +182,17 @@ export class RedisClientMock extends EventEmitter { key: string, start: number, stop: number, - cb?: Function + cb?: Function, ): boolean { const q = RedisClientMock.__queues__[key] = RedisClientMock.__queues__[key] || []; - cb && cb(null, q.splice(start, stop)[0]); - return true; + const result = q.splice(start, stop); + this.cbExecute(cb, null, result); + return result; } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public scan(...args: any[]): boolean { + public scan(...args: any[]): (string | string[])[] { const cb = args.pop(); const qs = RedisClientMock.__queues__; const found: string[] = []; @@ -192,65 +201,97 @@ export class RedisClientMock extends EventEmitter { found.push(q); } } - cb && cb(null, ['0', found]); - return true; + const result = ['0', found]; + this.cbExecute(cb, null, result); + return result; } // noinspection JSMethodCanBeStatic - public script(...args: any[]): boolean { + public script(...args: any[]): unknown { const cmd = args.shift(); - const script = args.shift(); - let hash: any = ''; - if (cmd === 'load') { - hash = sha1(script); - RedisClientMock.__scripts[hash] = script; + const scriptOrHash = args.shift(); + const cb = args.pop(); + const isCb = typeof cb === 'function'; + + if (cmd === 'LOAD') { + const hash = sha1(scriptOrHash); + RedisClientMock.__scripts[hash] = scriptOrHash; + isCb && cb(null, hash); + return hash; } - if (cmd === 'exists') { - hash = RedisClientMock.__scripts[hash] !== undefined; + if (cmd === 'EXISTS') { + const hash = RedisClientMock.__scripts[scriptOrHash] !== undefined; + + isCb && cb(null, hash); + + return [Number(hash)]; } - args.pop()(null, hash); - return true; + + return [0]; } // noinspection JSUnusedGlobalSymbols - public client(...args: any[]): boolean { + public client(...args: any[]): string | boolean { const self = RedisClientMock; - const cb = args.pop(); const cmd = args.shift(); + const cb = args.pop(); const name = args.shift(); - if (cmd === 'list') { - return cb(null, Object.keys(self.__clientList) - .map((name: string, id: number) => `id=${id} name=${name}`) - .join('\n')); + + if (cmd === 'LIST') { + const result = Object.keys(self.__clientList) + .map((name: string, id: number) => `id=${id} name=${name}`) + .join('\n'); + + this.cbExecute(cb, null, result); + return result; } - else if (cmd === 'setname') { + else if (cmd === 'SETNAME') { this.__name = name; self.__clientList[name] = true; } - cb(null, true); + this.cbExecute(cb, null, true); return true; } // noinspection JSMethodCanBeStatic public exists(...args: any[]): boolean { const key = args.shift(); - args.pop()(null, RedisClientMock.__keys[key] !== undefined); - return true; + const result = RedisClientMock.__keys[key] !== undefined; + this.cbExecute(args.pop(), null, result); + return result; } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public psubscribe(...args: any[]): boolean { - args.pop()(null, 1); - return true; + public async psubscribe(...args: any[]): Promise { + this.cbExecute(args.pop(), null, 1); + return 1; } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic public evalsha(...args: any[]): boolean { - args.pop()(); + this.cbExecute(args.pop()); return true; } + // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic + public async del(...args: any[]): Promise { + const self = RedisClientMock; + let count = 0; + for (let key of args) { + if (self.__keys[key] !== undefined) { + delete self.__keys[key]; + count++; + } + if (self.__queues__[key] !== undefined) { + delete self.__queues__[key]; + count++; + } + } + this.cbExecute(args.pop(), count); + return count; + } + // noinspection JSUnusedGlobalSymbols public zadd(...args: any[]): boolean { const [key, score, value, cb] = args; @@ -259,12 +300,12 @@ export class RedisClientMock extends EventEmitter { const toKey = key.split(/:/).slice(0,2).join(':'); this.lpush(toKey, value); }, timeout); - cb && cb(); + this.cbExecute(cb); return true; } // noinspection JSUnusedGlobalSymbols - public unref(): boolean { + public disconnect(): boolean { delete RedisClientMock.__clientList[this.__name]; if (this.__rt) { clearTimeout(this.__rt); @@ -274,23 +315,24 @@ export class RedisClientMock extends EventEmitter { } // noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic - public config(): boolean { + public async config(): Promise { return true; } + private cbExecute(cb: any, ...args: any[]): void { + if (typeof cb === 'function') { + cb(...args); + } + } } -/** - * @implements {IMulti} - */ -export class RedisMultiMock extends EventEmitter {} +const Redis = RedisClientMock; + +mock('ioredis', { Redis, default: Redis }); -mock('redis', { - createClient() { return new RedisClientMock() }, - RedisClient: RedisClientMock, - Multi: RedisMultiMock -}); +// @ts-ignore +export * from 'ioredis'; -import * as redis from 'redis'; +export { Redis }; -export { redis }; +export default Redis;