├── .editorconfig ├── .env.example ├── .github └── workflows │ ├── feature.yml │ ├── staging.yml │ └── tag.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── IdDeterministic.html │ ├── IdInternal.html │ ├── IdRandom.html │ └── IdSortable-1.html ├── functions │ ├── idSortable.extractRand.html │ ├── idSortable.extractSeq.html │ ├── idSortable.extractTs.html │ ├── utils.bits2bytes.html │ ├── utils.bytes2bits.html │ ├── utils.bytes2hex.html │ ├── utils.dec2bits.html │ ├── utils.dec2hex.html │ ├── utils.equals.html │ ├── utils.fromBuffer.html │ ├── utils.fromFixedPoint.html │ ├── utils.fromJSON.html │ ├── utils.fromMultibase.html │ ├── utils.fromString.html │ ├── utils.fromUUID.html │ ├── utils.hex2bytes.html │ ├── utils.nodeBits.html │ ├── utils.randomBits.html │ ├── utils.randomBytes.html │ ├── utils.roundPrecise.html │ ├── utils.strChunks.html │ ├── utils.take.html │ ├── utils.timeSource.html │ ├── utils.toBuffer.html │ ├── utils.toFixedPoint.html │ ├── utils.toJSON.html │ ├── utils.toMultibase.html │ ├── utils.toString.html │ └── utils.toUUID.html ├── index.html ├── modules.html ├── modules │ ├── idSortable.html │ └── utils.html └── types │ ├── Id.html │ └── utils.MultibaseFormats.html ├── flake.lock ├── flake.nix ├── jest.config.mjs ├── package-lock.json ├── package.json ├── scripts ├── brew-install.sh ├── choco-install.ps1 └── test.mjs ├── src ├── Id.ts ├── IdDeterministic.ts ├── IdRandom.ts ├── IdSortable.ts ├── index.ts └── utils.ts ├── tests ├── Id.test.ts ├── IdDeterministic.test.ts ├── IdRandom.test.ts ├── IdSortable.test.ts ├── global.d.ts ├── globalSetup.ts ├── globalTeardown.ts ├── setup.ts ├── setupAfterEnv.ts ├── utils.test.ts └── utils.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-id/9654b7fd0c75e31f357af6cbb7a8af849974503f/.env.example -------------------------------------------------------------------------------- /.github/workflows/feature.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Feature" 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature* 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | use-library-js-feature: 15 | permissions: 16 | contents: read 17 | actions: write 18 | checks: write 19 | uses: MatrixAI/.github/.github/workflows/library-js-feature.yml@master 20 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Staging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | use-library-js-staging: 15 | permissions: 16 | contents: write 17 | actions: write 18 | checks: write 19 | pull-requests: write 20 | uses: MatrixAI/.github/.github/workflows/library-js-staging.yml@master 21 | secrets: 22 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 23 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 24 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 25 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} 26 | GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Tag" 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | use-library-js-tag: 11 | permissions: 12 | contents: read 13 | actions: write 14 | uses: MatrixAI/.github/.github/workflows/library-js-tag.yml@master 15 | secrets: 16 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /dist 3 | .env* 4 | !.env.example 5 | # nix 6 | /result* 7 | /builds 8 | # node-gyp 9 | /build 10 | # prebuildify 11 | /prebuilds 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | 21 | # Diagnostic reports (https://nodejs.org/api/report.html) 22 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 41 | .grunt 42 | 43 | # Bower dependency directory (https://bower.io/) 44 | bower_components 45 | 46 | # node-waf configuration 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | build/Release 51 | 52 | # Dependency directories 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | web_modules/ 58 | 59 | # TypeScript cache 60 | *.tsbuildinfo 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variables file 84 | .env 85 | .env.test 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # Serverless directories 109 | .serverless/ 110 | 111 | # FuseBox cache 112 | .fusebox/ 113 | 114 | # DynamoDB Local files 115 | .dynamodb/ 116 | 117 | # TernJS port file 118 | .tern-port 119 | 120 | # Stores VSCode versions used for testing VSCode extensions 121 | .vscode-test 122 | 123 | # yarn v2 124 | .yarn/cache 125 | .yarn/unplugged 126 | .yarn/build-state.yml 127 | .yarn/install-state.gz 128 | .pnp.* 129 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /*.nix 3 | /nix 4 | /tsconfig.json 5 | /tsconfig.build.json 6 | /jest.config.js 7 | /scripts 8 | /src 9 | /tests 10 | /tmp 11 | /docs 12 | /benches 13 | /build 14 | /builds 15 | /dist/tsbuildinfo 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-id 2 | 3 | ID generation for JavaScript & TypeScript applications. 4 | 5 | Example Usage: 6 | 7 | ```ts 8 | import { IdRandom, IdDeterministic, IdSortable, utils } from '@matrixai/id'; 9 | 10 | // Random ids, equivalent to UUIDv4 11 | 12 | const randGen = new IdRandom(); 13 | 14 | const randIds = [...utils.take(randGen, 3)]; 15 | console.log(randIds.map((b) => utils.toUUID(b))); 16 | 17 | // Deterministic ids, equivalent to UUIDv5 18 | 19 | const deteGen = new IdDeterministic({ 20 | namespace: 'foo', 21 | }); 22 | 23 | const deteId1 = deteGen.get(); 24 | const deteId2 = deteGen.get('bar'); 25 | const deteId3 = deteGen.get('bar'); 26 | 27 | console.log(utils.toUUID(deteId1)); 28 | console.log(utils.toMultibase(deteId2, 'base32hex')); 29 | 30 | // Will be cast to string index 31 | const recordOfDeteIds = {}; 32 | recordOfDeteIds[deteId1] = 1; 33 | recordOfDeteIds[deteId2] = 1; 34 | console.log(recordOfDeteIds[deteId1]); 35 | 36 | // Can be checked for equality 37 | console.log(deteId2.equals(deteId3)); 38 | // Binary string form can be checked for equality 39 | console.log(deteId2.toString() === deteId3.toString()); 40 | 41 | // Strictly monotonic sortable ids, equivalent to UUIDv7 42 | 43 | let lastId = new Uint8Array([ 44 | 0x06, 0x16, 0x3e, 0xf5, 0x6d, 0x8d, 0x70, 0x00, 0x87, 0xc4, 0x65, 0xd5, 0x21, 45 | 0x9b, 0x03, 0xd4, 46 | ]); 47 | 48 | const sortGen = new IdSortable({ lastId }); 49 | 50 | const sortId1 = sortGen.get(); 51 | const sortId2 = sortGen.get(); 52 | const sortId3 = sortGen.get(); 53 | 54 | const sortIds = [ 55 | utils.toBuffer(sortId2), 56 | utils.toBuffer(sortId3), 57 | utils.toBuffer(sortId1), 58 | ]; 59 | 60 | sortIds.sort(Buffer.compare); 61 | 62 | console.log(sortIds); 63 | 64 | // Save the last id to ensure strict monotonicity across process restarts 65 | lastId = sortGen.lastId; 66 | 67 | // Ids can also be compared in order 68 | console.log(sortId1 < sortId2); 69 | console.log(sortId2 < sortId3); 70 | ``` 71 | 72 | **Base Encoding and Lexicographic Order** 73 | 74 | It is important to realise that not all base-encodings preserve lexicographic 75 | sort order. The UUID (hex-encoding) and `base32hex` does, but `base58btc` and 76 | `base64` does not. Make sure to pick an appropriate base encoding if you are 77 | expecting to compare the `IdSortable` as base-encoded strings. 78 | 79 | Out of all the multibase encodings, the only ones that preserve sort order are: 80 | 81 | ``` 82 | base2 83 | base8 84 | base16 85 | base16upper 86 | base32hex 87 | base32hexupper 88 | base32hexpad 89 | base32hexpadupper 90 | ``` 91 | 92 | In addition to this, JS binary string encoding through `id.toString()` also 93 | preserves sort order. 94 | 95 | ## Installation 96 | 97 | ```sh 98 | npm install --save @matrixai/id 99 | ``` 100 | 101 | ## Development 102 | 103 | Run `nix develop`, and once you're inside, you can use: 104 | 105 | ```sh 106 | # install (or reinstall packages from package.json) 107 | npm install 108 | # build the dist 109 | npm run build 110 | # run the repl (this allows you to import from ./src) 111 | npm run tsx 112 | # run the tests 113 | npm run test 114 | # lint the source code 115 | npm run lint 116 | # automatically fix the source 117 | npm run lintfix 118 | ``` 119 | 120 | ### Docs Generation 121 | 122 | ```sh 123 | npm run docs 124 | ``` 125 | 126 | See the docs at: https://matrixai.github.io/js-id/ 127 | 128 | ### Publishing 129 | 130 | Publishing is handled automatically by the staging pipeline. 131 | 132 | Prerelease: 133 | 134 | ```sh 135 | # npm login 136 | npm version prepatch --preid alpha # premajor/preminor/prepatch 137 | git push --follow-tags 138 | ``` 139 | 140 | Release: 141 | 142 | ```sh 143 | # npm login 144 | npm version patch # major/minor/patch 145 | git push --follow-tags 146 | ``` 147 | 148 | Manually: 149 | 150 | ```sh 151 | # npm login 152 | npm version patch # major/minor/patch 153 | npm run build 154 | npm publish --access public 155 | git push 156 | git push --tags 157 | ``` 158 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #AF00DB; 3 | --dark-hl-0: #C586C0; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0000FF; 13 | --dark-hl-5: #569CD6; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #795E26; 17 | --dark-hl-7: #DCDCAA; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/functions/idSortable.extractRand.html: -------------------------------------------------------------------------------- 1 | extractRand | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function extractRand

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      idBytes: Uint8Array
    28 |

    Returns string

31 |
32 | 46 |
63 |
64 |

Generated using TypeDoc

65 |
-------------------------------------------------------------------------------- /docs/functions/idSortable.extractSeq.html: -------------------------------------------------------------------------------- 1 | extractSeq | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function extractSeq

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      idBytes: Uint8Array
    28 |

    Returns number

31 |
32 | 46 |
63 |
64 |

Generated using TypeDoc

65 |
-------------------------------------------------------------------------------- /docs/functions/idSortable.extractTs.html: -------------------------------------------------------------------------------- 1 | extractTs | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function extractTs

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      idBytes: Uint8Array
    28 |

    Returns number

31 |
32 | 46 |
63 |
64 |

Generated using TypeDoc

65 |
-------------------------------------------------------------------------------- /docs/functions/utils.hex2bytes.html: -------------------------------------------------------------------------------- 1 | hex2bytes | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function hex2bytes

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      hex: string
    28 |

    Returns Uint8Array

31 |
32 | 46 |
86 |
87 |

Generated using TypeDoc

88 |
-------------------------------------------------------------------------------- /docs/functions/utils.toString.html: -------------------------------------------------------------------------------- 1 | toString | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function toString

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      id: Uint8Array
    28 |

    Returns string

31 |
32 | 46 |
86 |
87 |

Generated using TypeDoc

88 |
-------------------------------------------------------------------------------- /docs/functions/utils.toUUID.html: -------------------------------------------------------------------------------- 1 | toUUID | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Function toUUID

19 |
20 |
    21 | 22 |
  • 23 |
    24 |

    Parameters

    25 |
      26 |
    • 27 |
      id: Uint8Array
    28 |

    Returns string

31 |
32 | 46 |
86 |
87 |

Generated using TypeDoc

88 |
-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 |

@matrixai/id

15 |
16 |
17 |

Index

18 |
19 |

Namespaces

20 |
idSortable 21 | utils 22 |
23 |
24 |

Classes

25 |
30 |
31 |

Type Aliases

32 |
Id 33 |
34 |
35 | 49 |
59 |
60 |

Generated using TypeDoc

61 |
-------------------------------------------------------------------------------- /docs/modules/idSortable.html: -------------------------------------------------------------------------------- 1 | idSortable | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Namespace idSortable

20 |
21 |
22 |
23 | 24 |
25 |
26 |

References

27 |
default 28 |
29 |
30 |

Functions

31 |
35 |
36 |

References

37 |
38 | Renames and re-exports IdSortable
39 |
40 | 59 |
76 |
77 |

Generated using TypeDoc

78 |
-------------------------------------------------------------------------------- /docs/types/Id.html: -------------------------------------------------------------------------------- 1 | Id | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias Id

18 |
Id: IdInternal & number
19 |

IdInternal can be used as a string primitive 20 | This type hack (as a number) prevents TS from complaining 21 | See: https://github.com/microsoft/TypeScript/issues/4538

22 |
23 |
26 |
27 | 41 |
51 |
52 |

Generated using TypeDoc

53 |
-------------------------------------------------------------------------------- /docs/types/utils.MultibaseFormats.html: -------------------------------------------------------------------------------- 1 | MultibaseFormats | @matrixai/id
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias MultibaseFormats

19 |
MultibaseFormats: keyof typeof bases
22 |
23 | 37 |
77 |
78 |

Generated using TypeDoc

79 |
-------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1736139540, 24 | "narHash": "sha256-39Iclrd+9tPLmvuFVyoG63WnHZJ9kCOC6eRytRYLAWw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "8ab83a21276434aaf44969b8dd0bc0e65b97a240", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "rev": "8ab83a21276434aaf44969b8dd0bc0e65b97a240", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-matrix": { 38 | "inputs": { 39 | "nixpkgs": "nixpkgs" 40 | }, 41 | "locked": { 42 | "lastModified": 1736140072, 43 | "narHash": "sha256-MgtcAA+xPldS0WlV16TjJ0qgFzGvKuGM9p+nPUxpUoA=", 44 | "owner": "MatrixAI", 45 | "repo": "nixpkgs-matrix", 46 | "rev": "029084026bc4a35bce81bac898aa695f41993e18", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "id": "nixpkgs-matrix", 51 | "type": "indirect" 52 | } 53 | }, 54 | "root": { 55 | "inputs": { 56 | "flake-utils": "flake-utils", 57 | "nixpkgs-matrix": "nixpkgs-matrix" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs-matrix = { 4 | type = "indirect"; 5 | id = "nixpkgs-matrix"; 6 | }; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { nixpkgs-matrix, flake-utils, ... }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = nixpkgs-matrix.legacyPackages.${system}; 14 | shell = { ci ? false }: 15 | with pkgs; 16 | pkgs.mkShell { 17 | nativeBuildInputs = [ nodejs_20 shellcheck gitAndTools.gh ]; 18 | PKG_IGNORE_TAG = 1; 19 | shellHook = '' 20 | echo "Entering $(npm pkg get name)" 21 | set -o allexport 22 | . <(polykey secrets env js-id) 23 | set +o allexport 24 | set -v 25 | ${lib.optionalString ci '' 26 | set -o errexit 27 | set -o nounset 28 | set -o pipefail 29 | shopt -s inherit_errexit 30 | ''} 31 | mkdir --parents "$(pwd)/tmp" 32 | 33 | export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" 34 | 35 | npm install --ignore-scripts 36 | 37 | set +v 38 | ''; 39 | }; 40 | in { 41 | devShells = { 42 | default = shell { ci = false; }; 43 | ci = shell { ci = true; }; 44 | }; 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import url from 'node:url'; 3 | import tsconfigJSON from './tsconfig.json' assert { type: "json" }; 4 | 5 | const projectPath = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | // Global variables that are shared across the jest worker pool 8 | // These variables must be static and serializable 9 | const globals = { 10 | // Absolute directory to the project root 11 | projectDir: projectPath, 12 | // Absolute directory to the test root 13 | testDir: path.join(projectPath, 'tests'), 14 | // Default asynchronous test timeout 15 | defaultTimeout: 20000, 16 | // Timeouts rely on setTimeout which takes 32 bit numbers 17 | maxTimeout: Math.pow(2, 31) - 1, 18 | }; 19 | 20 | // The `globalSetup` and `globalTeardown` cannot access the `globals` 21 | // They run in their own process context 22 | // They can however receive the process environment 23 | // Use `process.env` to set variables 24 | 25 | const config = { 26 | testEnvironment: 'node', 27 | verbose: true, 28 | collectCoverage: false, 29 | cacheDirectory: '/tmp/jest', 30 | coverageDirectory: '/tmp/coverage', 31 | roots: ['/tests'], 32 | testMatch: ['**/?(*.)+(spec|test|unit.test).+(ts|tsx|js|jsx)'], 33 | transform: { 34 | "^.+\\.(t|j)sx?$": [ 35 | "@swc/jest", 36 | { 37 | jsc: { 38 | parser: { 39 | syntax: "typescript", 40 | tsx: true, 41 | decorators: tsconfigJSON.compilerOptions.experimentalDecorators, 42 | dynamicImport: true, 43 | }, 44 | target: tsconfigJSON.compilerOptions.target.toLowerCase(), 45 | keepClassNames: true, 46 | }, 47 | } 48 | ], 49 | }, 50 | reporters: [ 51 | 'default', 52 | ['jest-junit', { 53 | outputDirectory: '/tmp/junit', 54 | classNameTemplate: '{classname}', 55 | ancestorSeparator: ' > ', 56 | titleTemplate: '{title}', 57 | addFileAttribute: 'true', 58 | reportTestSuiteErrors: 'true', 59 | }], 60 | ], 61 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'], 62 | coverageReporters: ['text', 'cobertura'], 63 | globals, 64 | // Global setup script executed once before all test files 65 | globalSetup: '/tests/globalSetup.ts', 66 | // Global teardown script executed once after all test files 67 | globalTeardown: '/tests/globalTeardown.ts', 68 | // Setup files are executed before each test file 69 | // Can access globals 70 | setupFiles: ['/tests/setup.ts'], 71 | // Setup files after env are executed before each test file 72 | // after the jest test environment is installed 73 | // Can access globals 74 | setupFilesAfterEnv: [ 75 | 'jest-extended/all', 76 | '/tests/setupAfterEnv.ts' 77 | ], 78 | moduleNameMapper: { 79 | "^(\\.{1,2}/.*)\\.js$": "$1", 80 | }, 81 | extensionsToTreatAsEsm: ['.ts', '.tsx', '.mts'], 82 | }; 83 | 84 | export default config; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrixai/id", 3 | "version": "4.0.0", 4 | "author": "Roger Qiu", 5 | "description": "ID generation for JavaScript & TypeScript Applications", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/MatrixAI/js-id.git" 10 | }, 11 | "type": "module", 12 | "exports": { 13 | "./package.json": "./package.json", 14 | ".": { 15 | "types": "./dist/index.d.ts", 16 | "import": "./dist/index.js" 17 | }, 18 | "./*.js": { 19 | "types": "./dist/*.d.ts", 20 | "import": "./dist/*.js" 21 | }, 22 | "./*": "./dist/*" 23 | }, 24 | "imports": { 25 | "#*": "./dist/*" 26 | }, 27 | "scripts": { 28 | "prepare": "tsc -p ./tsconfig.build.json", 29 | "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json", 30 | "postversion": "npm install --package-lock-only --ignore-scripts --silent", 31 | "tsx": "tsx", 32 | "test": "node ./scripts/test.mjs", 33 | "lint": "matrixai-lint", 34 | "lintfix": "matrixai-lint --fix", 35 | "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src" 36 | }, 37 | "dependencies": { 38 | "multiformats": "^13.3.2", 39 | "uuid": "^8.3.2" 40 | }, 41 | "devDependencies": { 42 | "@matrixai/lint": "^0.2.11", 43 | "@swc/core": "^1.3.62", 44 | "@swc/jest": "^0.2.29", 45 | "@types/jest": "^29.5.2", 46 | "@types/node": "^20.5.7", 47 | "jest": "^29.6.2", 48 | "jest-extended": "^4.0.0", 49 | "jest-junit": "^16.0.0", 50 | "shx": "^0.3.4", 51 | "tsx": "^3.12.7", 52 | "typedoc": "^0.24.8", 53 | "typescript": "^5.1.6" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/brew-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | export HOMEBREW_NO_INSTALL_UPGRADE=1 8 | export HOMEBREW_NO_INSTALL_CLEANUP=1 9 | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 10 | export HOMEBREW_NO_AUTO_UPDATE=1 11 | export HOMEBREW_NO_ANALYTICS=1 12 | 13 | brew reinstall node@20 14 | brew link --overwrite node@20 15 | -------------------------------------------------------------------------------- /scripts/choco-install.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | function Save-ChocoPackage { 4 | param ( 5 | $PackageName 6 | ) 7 | Rename-Item -Path "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg" -NewName "$PackageName.nupkg.zip" -ErrorAction:SilentlyContinue 8 | Expand-Archive -LiteralPath "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg.zip" -DestinationPath "$env:ChocolateyInstall\lib\$PackageName" -Force 9 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\_rels" -Recurse 10 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\package" -Recurse 11 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\[Content_Types].xml" 12 | New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" -ItemType "directory" -ErrorAction:SilentlyContinue 13 | choco pack "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nuspec" --outdir "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" 14 | } 15 | 16 | # Check for existence of required environment variables 17 | if ( $null -eq $env:ChocolateyInstall ) { 18 | [Console]::Error.WriteLine('Missing $env:ChocolateyInstall environment variable') 19 | exit 1 20 | } 21 | 22 | # Add the cached packages with source priority 1 (Chocolatey community is 0) 23 | New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey" -ItemType "directory" -ErrorAction:SilentlyContinue 24 | choco source add --name="cache" --source="${PSScriptRoot}\..\tmp\chocolatey" --priority=1 25 | 26 | # Install nodejs v20.5.1 (will use cache if exists) 27 | $nodejs = "nodejs" 28 | choco install "$nodejs" --version="20.5.1" --require-checksums -y 29 | # Internalise nodejs to cache if doesn't exist 30 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nodejs\$nodejs.20.5.1.nupkg" -PathType Leaf) ) { 31 | Save-ChocoPackage -PackageName $nodejs 32 | } 33 | -------------------------------------------------------------------------------- /scripts/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | import url from 'node:url'; 6 | import process from 'node:process'; 7 | import childProcess from 'node:child_process'; 8 | 9 | const projectPath = path.dirname( 10 | path.dirname(url.fileURLToPath(import.meta.url)), 11 | ); 12 | 13 | const platform = os.platform(); 14 | 15 | /* eslint-disable no-console */ 16 | async function main(argv = process.argv) { 17 | argv = argv.slice(2); 18 | const tscArgs = [`-p`, path.join(projectPath, 'tsconfig.build.json')]; 19 | console.error('Running tsc:'); 20 | console.error(['tsc', ...tscArgs].join(' ')); 21 | childProcess.execFileSync('tsc', tscArgs, { 22 | stdio: ['inherit', 'inherit', 'inherit'], 23 | windowsHide: true, 24 | encoding: 'utf-8', 25 | shell: platform === 'win32' ? true : false, 26 | }); 27 | const jestArgs = [...argv]; 28 | console.error('Running jest:'); 29 | console.error(['jest', ...jestArgs].join(' ')); 30 | childProcess.execFileSync('jest', jestArgs, { 31 | env: { 32 | ...process.env, 33 | NODE_OPTIONS: '--experimental-vm-modules', 34 | }, 35 | stdio: ['inherit', 'inherit', 'inherit'], 36 | windowsHide: true, 37 | encoding: 'utf-8', 38 | shell: platform === 'win32' ? true : false, 39 | }); 40 | } 41 | /* eslint-enable no-console */ 42 | 43 | if (import.meta.url.startsWith('file:')) { 44 | const modulePath = url.fileURLToPath(import.meta.url); 45 | if (process.argv[1] === modulePath) { 46 | void main(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Id.ts: -------------------------------------------------------------------------------- 1 | import type { MultibaseFormats } from './utils.js'; 2 | import * as utils from './utils.js'; 3 | 4 | /** 5 | * IdInternal can be used as a string primitive 6 | * This type hack (as a number) prevents TS from complaining 7 | * See: https://github.com/microsoft/TypeScript/issues/4538 8 | */ 9 | type Id = IdInternal & number; 10 | 11 | class IdInternal extends Uint8Array { 12 | public static create(): T; 13 | public static create(id: T): T; 14 | public static create(length: number): T; 15 | public static create( 16 | array: ArrayLike | ArrayBufferLike, 17 | ): T; 18 | public static create( 19 | buffer: ArrayBufferLike, 20 | byteOffset?: number, 21 | length?: number, 22 | ): T; 23 | public static create(...args: Array): T { 24 | // @ts-ignore: spreading into Uint8Array constructor 25 | return new IdInternal(...args) as T; 26 | } 27 | 28 | public static fromString(idString: string): T { 29 | return utils.fromString(idString) as T; 30 | } 31 | 32 | public static fromJSON(json: any): T | undefined { 33 | return utils.fromJSON(json) as T; 34 | } 35 | 36 | /** 37 | * Decodes as Buffer zero-copy 38 | */ 39 | public static fromBuffer(idBuffer: Buffer): T { 40 | return utils.fromBuffer(idBuffer) as T; 41 | } 42 | 43 | public static fromUUID(uuid: string): T | undefined { 44 | return utils.fromUUID(uuid) as T; 45 | } 46 | 47 | public static fromMultibase( 48 | idString: string, 49 | ): T | undefined { 50 | return utils.fromMultibase(idString) as T; 51 | } 52 | 53 | public [Symbol.toPrimitive](_hint: 'string' | 'number' | 'default'): string { 54 | return utils.toString(this as unknown as Id); 55 | } 56 | 57 | public toString(): string { 58 | return utils.toString(this as unknown as Id); 59 | } 60 | 61 | public toJSON(): { type: string; data: Array } { 62 | return utils.toJSON(this); 63 | } 64 | 65 | /** 66 | * Encodes as Buffer zero-copy 67 | */ 68 | public toBuffer(): Buffer { 69 | return utils.toBuffer(this); 70 | } 71 | 72 | /** 73 | * Encodes as a 16 byte UUID 74 | * This only works when the Id is 16 bytes long 75 | */ 76 | public toUUID(): string { 77 | return utils.toUUID(this); 78 | } 79 | 80 | /** 81 | * Encodes an multibase ID string 82 | */ 83 | public toMultibase(format: MultibaseFormats): string { 84 | return utils.toMultibase(this, format); 85 | } 86 | 87 | /** 88 | * Efficiently compares for equality 89 | * This is faster than comparing by binary string 90 | * If you have an ArrayBuffer, wrap it in Uint8Array first 91 | */ 92 | public equals(id: Uint8Array): boolean { 93 | return utils.equals(this, id); 94 | } 95 | } 96 | 97 | export default IdInternal; 98 | 99 | export type { Id }; 100 | -------------------------------------------------------------------------------- /src/IdDeterministic.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from './Id.js'; 2 | import { v5 as uuidv5, NIL } from 'uuid'; 3 | import IdInternal from './Id.js'; 4 | 5 | /** 6 | * This produces deterministic ids based on: 7 | * UUIDv5( 8 | * SHA1(namespaceUUID + name) 9 | * where 10 | * namespaceUUID is SHA1(NIL UUID + namespace) 11 | * ) 12 | */ 13 | class IdDeterministic implements IterableIterator { 14 | protected namespaceData: Uint8Array; 15 | 16 | public constructor({ 17 | namespace = '', 18 | }: { 19 | namespace?: string; 20 | } = {}) { 21 | const namespaceData = new Uint8Array(16); 22 | uuidv5(namespace, NIL, namespaceData); 23 | this.namespaceData = namespaceData; 24 | } 25 | 26 | public get(name?: string): T { 27 | return this.next(name).value as T; 28 | } 29 | 30 | public next(name: string = ''): IteratorResult { 31 | const id = IdInternal.create(16); 32 | uuidv5(name, this.namespaceData, id); 33 | return { 34 | value: id, 35 | done: false, 36 | }; 37 | } 38 | 39 | public [Symbol.iterator](): IterableIterator { 40 | return this; 41 | } 42 | } 43 | 44 | export default IdDeterministic; 45 | -------------------------------------------------------------------------------- /src/IdRandom.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from './Id.js'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import * as utils from './utils.js'; 4 | import IdInternal from './Id.js'; 5 | 6 | class IdRandom implements IterableIterator { 7 | protected randomSource: (size: number) => Uint8Array; 8 | 9 | public constructor({ 10 | randomSource = utils.randomBytes, 11 | }: { 12 | randomSource?: (size: number) => Uint8Array; 13 | } = {}) { 14 | this.randomSource = randomSource; 15 | } 16 | 17 | public get(): T { 18 | return this.next().value as T; 19 | } 20 | 21 | public next(): IteratorResult { 22 | const id = IdInternal.create(16); 23 | // `uuidv4` mutates the random data 24 | uuidv4( 25 | { 26 | rng: () => this.randomSource(16), 27 | }, 28 | id, 29 | ); 30 | return { 31 | value: id, 32 | done: false, 33 | }; 34 | } 35 | 36 | public [Symbol.iterator](): IterableIterator { 37 | return this; 38 | } 39 | } 40 | 41 | export default IdRandom; 42 | -------------------------------------------------------------------------------- /src/IdSortable.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from './Id.js'; 2 | import IdInternal from './Id.js'; 3 | import * as utils from './utils.js'; 4 | 5 | /** 6 | * Constants for UUIDv7 with millisecond precision 7 | * 0 1 2 3 8 | * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 9 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 10 | * | unixts | 11 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 12 | * |unixts | msec | ver | seq | 13 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 14 | * |var| rand | 15 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 16 | * | rand | 17 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 18 | */ 19 | const unixtsSize = 36; 20 | const msecSize = 12; 21 | const seqSize = 12; 22 | const randSize = 62; 23 | const msecPrecision = 3; 24 | const variantBits = '10'; 25 | const versionBits = '0111'; 26 | 27 | function extractTs(idBytes: Uint8Array): number { 28 | // Decode the timestamp from the last id 29 | // the timestamp bits is 48 bits or 6 bytes 30 | // this creates a new zero-copy view 31 | const idTsBytes = idBytes.subarray(0, (unixtsSize + msecSize) / 8); 32 | const idTsBits = utils.bytes2bits(idTsBytes); 33 | const unixtsBits = idTsBits.substr(0, unixtsSize); 34 | const msecBits = idTsBits.substr(unixtsSize, unixtsSize + msecSize); 35 | const unixts = parseInt(unixtsBits, 2); 36 | const msec = parseInt(msecBits, 2); 37 | // Converting from second and subseconds 38 | return utils.fromFixedPoint([unixts, msec], msecSize, msecPrecision); 39 | } 40 | 41 | function extractSeq(idBytes: Uint8Array): number { 42 | const idSeqBytes = idBytes.subarray( 43 | (unixtsSize + msecSize) / 8, 44 | (unixtsSize + msecSize + 4 + seqSize) / 8, 45 | ); 46 | const idSeqBits = utils.bytes2bits(idSeqBytes).substr(4, seqSize); 47 | const seq = parseInt(idSeqBits, 2); 48 | return seq; 49 | } 50 | 51 | function extractRand(idBytes: Uint8Array): string { 52 | const idRandBytes = idBytes.subarray( 53 | (unixtsSize + msecSize + 4 + seqSize) / 8, 54 | ); 55 | const idRandBits = utils.bytes2bits(idRandBytes).substr(2); 56 | return idRandBits; 57 | } 58 | 59 | /** 60 | * Sortable ID generator based on UUIDv7 with millisecond resolution 61 | * 36 bits of unixts enables timestamps of 2177.59 years from 1970 62 | * (2**36)/(1*60*60*24*365.25) ~= 2177.59 years 63 | * Which means it will work until the year 4147 64 | * 12 bits seq enables 4096 ids per millisecond 65 | * After 4096, it rolls over 66 | */ 67 | class IdSortable implements IterableIterator { 68 | protected randomSource: (size: number) => Uint8Array; 69 | protected clock: () => number; 70 | protected nodeBits?: string; 71 | protected lastTs?: [number, number]; 72 | protected _lastId?: T; 73 | protected seqCounter: number; 74 | 75 | public constructor({ 76 | lastId, 77 | nodeId, 78 | timeSource = utils.timeSource, 79 | randomSource = utils.randomBytes, 80 | }: { 81 | lastId?: Uint8Array; 82 | nodeId?: Uint8Array; 83 | timeSource?: (lastTs?: number) => () => number; 84 | randomSource?: (size: number) => Uint8Array; 85 | } = {}) { 86 | this.randomSource = randomSource; 87 | if (lastId == null) { 88 | this.clock = timeSource(); 89 | } else { 90 | // Decode the timestamp from the last id 91 | const lastIdTs = extractTs(lastId); 92 | // TimeSource requires millisecond precision 93 | this.clock = timeSource(lastIdTs * 10 ** msecPrecision); 94 | } 95 | if (nodeId != null) { 96 | this.nodeBits = utils.nodeBits(nodeId, randSize); 97 | } 98 | } 99 | 100 | get lastId(): T { 101 | if (this._lastId == null) { 102 | throw new ReferenceError('lastId has not yet been generated'); 103 | } 104 | return this._lastId; 105 | } 106 | 107 | public get(): T { 108 | return this.next().value as T; 109 | } 110 | 111 | public next(): IteratorResult { 112 | // Clock returns millisecond precision 113 | const ts = this.clock() / 10 ** msecPrecision; 114 | // Converting to seconds and subseconds 115 | const [unixts, msec] = utils.toFixedPoint(ts, msecSize, msecPrecision); 116 | const unixtsBits = utils.dec2bits(unixts, unixtsSize); 117 | const msecBits = utils.dec2bits(msec, msecSize); 118 | if ( 119 | this.lastTs != null && 120 | this.lastTs[0] >= unixts && 121 | this.lastTs[1] >= msec 122 | ) { 123 | this.seqCounter += 1; 124 | } else { 125 | this.seqCounter = 0; 126 | } 127 | const seqBits = utils.dec2bits(this.seqCounter, seqSize); 128 | // NodeBits can be written to the most significant rand portion 129 | let randBits: string; 130 | if (this.nodeBits != null) { 131 | const randSize_ = randSize - this.nodeBits.length; 132 | randBits = this.nodeBits; 133 | if (randSize_ > 0) { 134 | randBits += utils.randomBits(this.randomSource, randSize_); 135 | } 136 | } else { 137 | randBits = utils.randomBits(this.randomSource, randSize); 138 | } 139 | const idBits = 140 | unixtsBits + msecBits + versionBits + seqBits + variantBits + randBits; 141 | const idBytes = utils.bits2bytes(idBits); 142 | const id = IdInternal.create(idBytes.buffer); 143 | // Save the fixed point timestamp 144 | this.lastTs = [unixts, msec]; 145 | this._lastId = id; 146 | return { 147 | value: id, 148 | done: false, 149 | }; 150 | } 151 | 152 | public [Symbol.iterator](): IterableIterator { 153 | return this; 154 | } 155 | } 156 | 157 | export default IdSortable; 158 | 159 | export { extractTs, extractSeq, extractRand }; 160 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Id } from './Id.js'; 2 | export { default as IdInternal } from './Id.js'; 3 | export { default as IdRandom } from './IdRandom.js'; 4 | export { default as IdDeterministic } from './IdDeterministic.js'; 5 | export { default as IdSortable } from './IdSortable.js'; 6 | export * as idSortable from './IdSortable.js'; 7 | export * as utils from './utils.js'; 8 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from './Id.js'; 2 | import crypto from 'crypto'; 3 | import { performance } from 'perf_hooks'; 4 | import { bases } from 'multiformats/basics'; 5 | import IdInternal from './Id.js'; 6 | 7 | /** 8 | * Gets random bytes as Uint8Array 9 | */ 10 | function randomBytes(size: number): Uint8Array { 11 | return crypto.randomBytes(size); 12 | } 13 | 14 | /** 15 | * Gets random bit string 16 | */ 17 | function randomBits( 18 | randomBytes: (sizeBytes: number) => Uint8Array, 19 | size: number, 20 | ): string { 21 | const bytes = randomBytes(Math.ceil(size / 8)); 22 | const bits = [...bytes].map((n) => dec2bits(n, 8)).join(''); 23 | return bits.substr(0, size); 24 | } 25 | 26 | function nodeBits(nodeBytes: Uint8Array, size: number): string { 27 | const bytes = nodeBytes.subarray( 28 | 0, 29 | Math.min(nodeBytes.byteLength, Math.ceil(size / 8)), 30 | ); 31 | const bits = [...bytes].map((n) => dec2bits(n, 8)).join(''); 32 | return bits.substr(0, size); 33 | } 34 | 35 | /** 36 | * Monotonic system time in milliseconds as a floating point number 37 | * Use last timestamp this to ensure monotonocity is preserved over process restarts 38 | * Not strictly monotonic, which means the same number may be returned 39 | */ 40 | function timeSource(lastTs?: number): () => number { 41 | // `performance.now()` is weakly monotonic 42 | let origin: number; 43 | if (lastTs != null && performance.timeOrigin <= lastTs) { 44 | origin = lastTs; 45 | return () => { 46 | let now = origin + performance.now(); 47 | if (now === lastTs) { 48 | // Only needed if performance.now() returns 0 49 | // this means no time has elapsed and now is equal to lastTs 50 | // plus 1 assumes lastTs integer-part represents the maximum precision 51 | // so 1 is the smallest unit of time to be added 52 | now += 1; 53 | } 54 | return now; 55 | }; 56 | } else { 57 | origin = performance.timeOrigin; 58 | return () => origin + performance.now(); 59 | } 60 | } 61 | 62 | /** 63 | * Take n items from an iterator 64 | */ 65 | function* take(g: Iterator, l: number): Generator { 66 | for (let i = 0; i < l; i++) { 67 | const item = g.next(); 68 | if (item.done) return; 69 | yield item.value; 70 | } 71 | } 72 | 73 | function equals(id1: Uint8Array, id2: Uint8Array): boolean { 74 | if (id1.byteLength !== id2.byteLength) return false; 75 | return id1.every((byte, i) => byte === id2[i]); 76 | } 77 | 78 | function toString(id: Uint8Array): string { 79 | return String.fromCharCode(...id); 80 | } 81 | 82 | function fromString(idString: string): Id { 83 | const id = IdInternal.create(idString.length); 84 | for (let i = 0; i < idString.length; i++) { 85 | id[i] = idString.charCodeAt(i); 86 | } 87 | return id; 88 | } 89 | 90 | function toJSON(id: IdInternal): { type: string; data: Array } { 91 | return { 92 | type: IdInternal.name, 93 | data: [...id], 94 | }; 95 | } 96 | 97 | /** 98 | * Converts JSON object as Id 99 | * This tries to strictly check that the object is equal to the output of `toJSON` 100 | * If the `data` array contains non-numbers, those bytes will become 0 101 | */ 102 | function fromJSON(idJSON: any): Id | undefined { 103 | if (typeof idJSON !== 'object' || idJSON == null) { 104 | return; 105 | } 106 | const keys = Object.getOwnPropertyNames(idJSON); 107 | if (keys.length !== 2 || !keys.includes('type') || !keys.includes('data')) { 108 | return; 109 | } 110 | if (idJSON.type !== IdInternal.name) { 111 | return; 112 | } 113 | if (!Array.isArray(idJSON.data)) { 114 | return; 115 | } 116 | return IdInternal.create(idJSON.data); 117 | } 118 | 119 | function toUUID(id: Uint8Array): string { 120 | if (id.byteLength !== 16) { 121 | throw new RangeError('UUID can only be created from buffers with 16 bytes'); 122 | } 123 | const uuidHex = bytes2hex(id); 124 | return [ 125 | uuidHex.substr(0, 8), 126 | uuidHex.substr(8, 4), 127 | uuidHex.substr(12, 4), 128 | uuidHex.substr(16, 4), 129 | uuidHex.substr(20, 12), 130 | ].join('-'); 131 | } 132 | 133 | function fromUUID(uuid: string): Id | undefined { 134 | const uuidHex = uuid.split('-').join(''); 135 | if (uuidHex.length !== 32) { 136 | return; 137 | } 138 | return IdInternal.create(hex2bytes(uuidHex).buffer); 139 | } 140 | 141 | type MultibaseFormats = keyof typeof bases; 142 | type Codec = (typeof bases)[MultibaseFormats]; 143 | 144 | const basesByPrefix: Record = {}; 145 | for (const k in bases) { 146 | const codec = bases[k]; 147 | basesByPrefix[codec.prefix] = codec; 148 | } 149 | 150 | /** 151 | * Encodes an multibase ID string 152 | */ 153 | function toMultibase(id: Uint8Array, format: MultibaseFormats): string { 154 | const codec = bases[format]; 155 | return codec.encode(id); 156 | } 157 | 158 | /** 159 | * Decodes a multibase encoded ID 160 | * Do not use this for generic multibase strings 161 | */ 162 | function fromMultibase(idString: string): Id | undefined { 163 | const prefix = idString[0]; 164 | const codec = basesByPrefix[prefix]; 165 | if (codec == null) { 166 | return; 167 | } 168 | let buffer: Uint8Array; 169 | try { 170 | buffer = codec.decode(idString); 171 | } catch { 172 | return; 173 | } 174 | return IdInternal.create(buffer); 175 | } 176 | 177 | /** 178 | * Encodes as Buffer zero-copy 179 | */ 180 | function toBuffer(id: Uint8Array): Buffer { 181 | return Buffer.from(id.buffer, id.byteOffset, id.byteLength); 182 | } 183 | 184 | /** 185 | * Decodes as Buffer zero-copy 186 | */ 187 | function fromBuffer(idBuffer: Buffer): Id { 188 | return IdInternal.create( 189 | idBuffer.buffer, 190 | idBuffer.byteOffset, 191 | idBuffer.byteLength, 192 | ); 193 | } 194 | 195 | /** 196 | * Encodes Uint8Array to hex string 197 | */ 198 | function bytes2hex(bytes: Uint8Array): string { 199 | return [...bytes].map((n) => dec2hex(n, 2)).join(''); 200 | } 201 | 202 | function hex2bytes(hex: string): Uint8Array { 203 | const numbers = strChunks(hex, 2).map((b) => parseInt(b, 16)); 204 | return new Uint8Array(numbers); 205 | } 206 | 207 | /** 208 | * Encodes Uint8Array to bit string 209 | */ 210 | function bytes2bits(bytes: Uint8Array): string { 211 | return [...bytes].map((n) => dec2bits(n, 8)).join(''); 212 | } 213 | 214 | /** 215 | * Decodes bit string to Uint8Array 216 | */ 217 | function bits2bytes(bits: string): Uint8Array { 218 | const numbers = strChunks(bits, 8).map((b) => parseInt(b, 2)); 219 | return new Uint8Array(numbers); 220 | } 221 | 222 | /** 223 | * Encodes positive base 10 numbers to bit string 224 | * Will output bits in big-endian order 225 | */ 226 | function dec2bits(dec: number, size?: number): string { 227 | if (dec < 0) throw RangeError('`dec` must be positive'); 228 | if (size != null) { 229 | if (size < 0) throw RangeError('`size` must be positive'); 230 | if (size === 0) return ''; 231 | dec %= 2 ** size; 232 | } else { 233 | size = 0; 234 | } 235 | return dec.toString(2).padStart(size, '0'); 236 | } 237 | 238 | /** 239 | * Encodes positive base 10 numbers to hex string 240 | * Will output hex in big-endian order 241 | */ 242 | function dec2hex(dec: number, size?: number): string { 243 | if (dec < 0) throw RangeError('`dec` must be positive'); 244 | if (size != null) { 245 | if (size < 0) throw RangeError('`size` must be positive'); 246 | if (size === 0) return ''; 247 | dec %= 16 ** size; 248 | } else { 249 | size = 0; 250 | } 251 | return dec.toString(16).padStart(size, '0'); 252 | } 253 | 254 | /** 255 | * Chunks strings into same size chunks 256 | * The last chunk will be smaller if a clean division is not possible 257 | */ 258 | function strChunks(str: string, size: number): Array { 259 | const chunkCount = Math.ceil(str.length / size); 260 | const chunks = new Array(chunkCount); 261 | let i = 0; 262 | let o = 0; 263 | for (; i < chunkCount; ++i, o += size) { 264 | chunks[i] = str.substr(o, size); 265 | } 266 | return chunks; 267 | } 268 | 269 | /** 270 | * Round to number of decimal points 271 | */ 272 | function roundPrecise(num: number, digits: number = 0, base: number = 10) { 273 | const pow = Math.pow(base, digits); 274 | return Math.round((num + Number.EPSILON) * pow) / pow; 275 | } 276 | 277 | /** 278 | * Converts floating point number to a fixed point tuple 279 | * Size is number of bits allocated for the fractional 280 | * Precision dictates a fixed number of decimal places for the fractional 281 | */ 282 | function toFixedPoint( 283 | floating: number, 284 | size: number, 285 | precision?: number, 286 | ): [number, number] { 287 | let integer = Math.trunc(floating); 288 | let fractional: number; 289 | if (precision == null) { 290 | fractional = floating % 1; 291 | } else { 292 | fractional = roundPrecise(floating % 1, precision); 293 | } 294 | // If the fractional is rounded to 1 295 | // then it should be added to the integer 296 | if (fractional === 1) { 297 | integer += fractional; 298 | fractional = 0; 299 | } 300 | // Floor is used to round down to a number that can be represented by the bit size 301 | // if ceil or round was used, it's possible to return a number that would overflow the bit size 302 | // for example if 12 bits is used, then 4096 would overflow to all zeros 303 | // the maximum for 12 bit is 4095 304 | const fractionalFixed = Math.floor(fractional * 2 ** size); 305 | return [integer, fractionalFixed]; 306 | } 307 | 308 | /** 309 | * Converts fixed point tuple to floating point number 310 | * Size is number of bits allocated for the fractional 311 | * Precision dictates a fixed number of decimal places for the fractional 312 | */ 313 | function fromFixedPoint( 314 | [integer, fractionalFixed]: [number, number], 315 | size: number, 316 | precision?: number, 317 | ): number { 318 | let fractional: number; 319 | if (precision == null) { 320 | fractional = fractionalFixed / 2 ** size; 321 | } else { 322 | fractional = roundPrecise(fractionalFixed / 2 ** size, precision); 323 | } 324 | return integer + fractional; 325 | } 326 | 327 | export { 328 | randomBytes, 329 | randomBits, 330 | nodeBits, 331 | timeSource, 332 | take, 333 | equals, 334 | toString, 335 | fromString, 336 | toJSON, 337 | fromJSON, 338 | toUUID, 339 | fromUUID, 340 | toMultibase, 341 | fromMultibase, 342 | toBuffer, 343 | fromBuffer, 344 | bytes2hex, 345 | hex2bytes, 346 | bytes2bits, 347 | bits2bytes, 348 | dec2bits, 349 | dec2hex, 350 | strChunks, 351 | roundPrecise, 352 | toFixedPoint, 353 | fromFixedPoint, 354 | }; 355 | 356 | export type { MultibaseFormats }; 357 | -------------------------------------------------------------------------------- /tests/Id.test.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '#Id.js'; 2 | import IdInternal from '#Id.js'; 3 | import * as utils from '#utils.js'; 4 | 5 | describe('Id', () => { 6 | test('create id from buffer', () => { 7 | const buffer = Buffer.from('abcefg'); 8 | const id = IdInternal.create(buffer); 9 | expect(id.toString()).toBe('abcefg'); 10 | expect(id + '').toBe('abcefg'); 11 | // Primitive value of Id is still Uint8Array 12 | expect([...id.valueOf()]).toStrictEqual([...buffer]); 13 | }); 14 | test('create id from id', () => { 15 | const buffer = Buffer.allocUnsafe(32); 16 | const id1 = IdInternal.create(buffer); 17 | const id2 = IdInternal.create(id1); 18 | expect([...id2]).toStrictEqual([...id1]); 19 | }); 20 | test('id can be used as POJO keys', () => { 21 | const buffer = Buffer.from('hello world'); 22 | const id = IdInternal.create(buffer); 23 | // Automatic string conversion takes place here 24 | // However the strings may not look very pretty 25 | // They are the raw binary string form 26 | // So if you expect to read this string on the terminal 27 | // Prefer to use an encoded form instead 28 | const pojo = { 29 | [id]: 'foo bar', 30 | }; 31 | expect(pojo[id]).toBe('foo bar'); 32 | expect(pojo[id.toString()]).toBe('foo bar'); 33 | for (const k of Object.keys(pojo)) { 34 | const idDecoded = utils.fromString(k); 35 | expect(idDecoded).toBeDefined(); 36 | expect([...idDecoded!]).toStrictEqual([...id]); 37 | break; 38 | } 39 | }); 40 | test('id can be used as Map keys', () => { 41 | const buffer = Buffer.from('hello world'); 42 | const id = IdInternal.create(buffer); 43 | // The id is an object, so this is an object key 44 | const map = new Map(); 45 | map.set(id, 'foo bar'); 46 | expect(map.get(id)).toBe('foo bar'); 47 | expect(map.get(id.toString())).toBeUndefined(); 48 | }); 49 | test('id can be created as a opaque type directly', async () => { 50 | type Opaque = T & { __TYPE__: K }; 51 | type OpaqueId = Opaque<'opaque', Id>; 52 | const buffer = Buffer.from('abcefg'); 53 | const id1 = IdInternal.create(buffer); 54 | expect(id1.toString()).toBe('abcefg'); 55 | expect(id1 + '').toBe('abcefg'); 56 | expect([...id1.valueOf()]).toStrictEqual([...buffer]); 57 | const id2: OpaqueId = IdInternal.create(buffer); 58 | expect(id2.toString()).toBe('abcefg'); 59 | expect(id2 + '').toBe('abcefg'); 60 | expect([...id2.valueOf()]).toStrictEqual([...buffer]); 61 | }); 62 | test('id encoding & decoding for multibase, string, uuid and buffer', async () => { 63 | type Opaque = T & { __TYPE__: K }; 64 | type OpaqueId = Opaque<'opaque', Id>; 65 | const buffer = Buffer.from('0123456789ABCDEF'); 66 | const id = IdInternal.create(buffer); 67 | const test1 = id.toMultibase('base32hex'); 68 | expect(IdInternal.fromMultibase(test1)).toStrictEqual(id); 69 | const test2 = id.toString(); 70 | expect(IdInternal.fromString(test2)).toStrictEqual(id); 71 | const test3 = id.toUUID(); 72 | expect(IdInternal.fromUUID(test3)).toStrictEqual(id); 73 | const test4 = id.toBuffer(); 74 | expect(IdInternal.fromBuffer(test4)).toStrictEqual(id); 75 | }); 76 | test('id equality', async () => { 77 | const id1 = IdInternal.create([97, 98, 99]); 78 | const id2 = IdInternal.create([97, 98, 99]); 79 | const id3 = IdInternal.create([97, 98, 100]); 80 | expect(id1.equals(id2)).toBe(true); 81 | expect(id1.equals(id3)).toBe(false); 82 | }); 83 | test('id JSON representation', async () => { 84 | const id = IdInternal.create([97, 98, 99]); 85 | const json1 = JSON.stringify(id); 86 | const id_ = JSON.parse(json1, (k, v) => { 87 | return IdInternal.fromJSON(v) ?? v; 88 | }); 89 | expect(id_).toBeInstanceOf(IdInternal); 90 | expect(id).toStrictEqual(id_); 91 | const json2 = JSON.stringify({ 92 | id, 93 | }); 94 | const object = JSON.parse(json2, (k, v) => { 95 | return IdInternal.fromJSON(v) ?? v; 96 | }); 97 | expect(object.id).toBeInstanceOf(IdInternal); 98 | expect(object.id).toStrictEqual(id); 99 | // Primitives should return undefined 100 | expect(IdInternal.fromJSON(123)).toBeUndefined(); 101 | expect(IdInternal.fromJSON('abc')).toBeUndefined(); 102 | expect(IdInternal.fromJSON(undefined)).toBeUndefined(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/IdDeterministic.test.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '#index.js'; 2 | import IdDeterministic from '#IdDeterministic.js'; 3 | import * as utils from '#utils.js'; 4 | 5 | describe('IdDeterministic', () => { 6 | test('ids are Uint8Array', () => { 7 | const idGen = new IdDeterministic(); 8 | const id = idGen.get(); 9 | expect(id).toBeInstanceOf(Uint8Array); 10 | }); 11 | test('ids can be generated', () => { 12 | const idGen = new IdDeterministic(); 13 | const ids = [...utils.take(idGen, 10)]; 14 | expect(ids).toHaveLength(10); 15 | }); 16 | test('ids can be generated as a opaque type', () => { 17 | // Can't really check that types are working besides building 18 | // This is more of an example 19 | type Opaque = T & { __TYPE__: K }; 20 | type OpaqueId = Opaque<'opaque', Id>; 21 | const idGen = new IdDeterministic(); 22 | const ids = [...utils.take(idGen, 10)]; 23 | expect(ids).toHaveLength(10); 24 | }); 25 | test('ids can be encoded and decoded as binary strings', () => { 26 | const idGen = new IdDeterministic(); 27 | const id = idGen.get(); 28 | const encoded = id.toString(); 29 | const id_ = utils.fromString(encoded); 30 | expect(id_).toBeDefined(); 31 | expect(utils.toBuffer(id).equals(utils.toBuffer(id_!))).toBe(true); 32 | }); 33 | test('ids can be encoded and decoded with multibase', () => { 34 | const idGen = new IdDeterministic(); 35 | const id = idGen.get(); 36 | const encoded = utils.toMultibase(id, 'base58btc'); 37 | const id_ = utils.fromMultibase(encoded); 38 | expect(id_).toBeDefined(); 39 | expect(Buffer.from(id).equals(Buffer.from(id_!))).toBe(true); 40 | }); 41 | test('ids can be encoded and decoded with uuid', () => { 42 | const idGen = new IdDeterministic(); 43 | const id = idGen.get(); 44 | const uuid = utils.toUUID(id); 45 | const id_ = utils.fromUUID(uuid); 46 | expect(id_).toBeDefined(); 47 | expect(Buffer.from(id).equals(Buffer.from(id_!))).toBe(true); 48 | }); 49 | test('ids are deterministic', () => { 50 | const id = new IdDeterministic(); 51 | const id1 = Buffer.from(id.get()); 52 | const id2 = Buffer.from(id.get()); 53 | expect(id1.equals(id2)).toBe(true); 54 | const id_ = new IdDeterministic({ 55 | namespace: 'abc', 56 | }); 57 | const id1_ = Buffer.from(id_.get()); 58 | const id2_ = Buffer.from(id_.get()); 59 | expect(id1_.equals(id2_)).toBe(true); 60 | expect(id1_.equals(id1)).toBe(false); 61 | const id3_ = Buffer.from(id_.get('foo')); 62 | const id4_ = Buffer.from(id_.get('bar')); 63 | expect(id3_.equals(id4_)).toBe(false); 64 | }); 65 | test('ids with different namespaces will generate different ids', () => { 66 | const idGen1 = new IdDeterministic({ 67 | namespace: 'foo', 68 | }); 69 | const idGen2 = new IdDeterministic({ 70 | namespace: 'bar', 71 | }); 72 | const idA1 = Buffer.from(idGen1.get('a')); 73 | const idA2 = Buffer.from(idGen2.get('a')); 74 | expect(idA1.equals(idA2)).toBe(false); 75 | const idB1 = Buffer.from(idGen1.get('b')); 76 | const idB2 = Buffer.from(idGen2.get('b')); 77 | expect(idB1.equals(idB2)).toBe(false); 78 | }); 79 | test('ids with the same namespace will generate the same ids', () => { 80 | const idGen1 = new IdDeterministic({ 81 | namespace: 'abc', 82 | }); 83 | const idGen2 = new IdDeterministic({ 84 | namespace: 'abc', 85 | }); 86 | const idA1 = Buffer.from(idGen1.get('a')); 87 | const idA2 = Buffer.from(idGen2.get('a')); 88 | expect(idA1.equals(idA2)).toBe(true); 89 | const idB1 = Buffer.from(idGen1.get('b')); 90 | const idB2 = Buffer.from(idGen2.get('b')); 91 | expect(idB1.equals(idB2)).toBe(true); 92 | }); 93 | test('ids can be used as record indexes', () => { 94 | const idGen = new IdDeterministic({ namespace: 'foo' }); 95 | const ids = [ 96 | idGen.get('a'), 97 | idGen.get('b'), 98 | idGen.get('c'), 99 | idGen.get('d'), 100 | ]; 101 | let counter = 0; 102 | const record = {}; 103 | for (const id of ids) { 104 | record[id] = counter; 105 | expect(record[id]).toBe(counter); 106 | counter++; 107 | } 108 | }); 109 | test('ids in strings can be compared for equality', () => { 110 | const idGen = new IdDeterministic({ namespace: 'foo' }); 111 | const id1 = idGen.get('a'); 112 | const id2 = idGen.get('a'); 113 | // Objects will be different 114 | expect(id1 == id2).toBe(false); // eslint-disable-line eqeqeq 115 | // Deterministic ids are the same 116 | expect(id1.toString() == id2.toString()).toBe(true); // eslint-disable-line eqeqeq 117 | expect(id1.toString()).toBe(id2 + ''); 118 | expect(id2.toString()).toBe(String(id1)); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/IdRandom.test.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '#index.js'; 2 | import IdRandom from '#IdRandom.js'; 3 | import * as utils from '#utils.js'; 4 | 5 | describe('IdRandom', () => { 6 | test('ids are Uint8Array', () => { 7 | const idGen = new IdRandom(); 8 | const id = idGen.get(); 9 | expect(id).toBeInstanceOf(Uint8Array); 10 | }); 11 | test('ids can be generated', () => { 12 | const idGen = new IdRandom(); 13 | const ids = [...utils.take(idGen, 10)]; 14 | expect(ids).toHaveLength(10); 15 | }); 16 | test('ids can be generated as OpaqueType', () => { 17 | // Can't really check that types are working besides building 18 | // This is more of an example 19 | type Opaque = T & { __TYPE__: K }; 20 | type OpaqueId = Opaque<'opaque', Id>; 21 | const idGen = new IdRandom(); 22 | const ids = [...utils.take(idGen, 10)]; 23 | expect(ids).toHaveLength(10); 24 | }); 25 | test('ids can be encoded and decoded as binary strings', () => { 26 | const idGen = new IdRandom(); 27 | const id = idGen.get(); 28 | const encoded = id.toString(); 29 | const id_ = utils.fromString(encoded); 30 | expect(id_).toBeDefined(); 31 | expect(utils.toBuffer(id).equals(utils.toBuffer(id_!))).toBe(true); 32 | }); 33 | test('ids can be encoded and decoded with multibase', () => { 34 | const idGen = new IdRandom(); 35 | const id = idGen.get(); 36 | const encoded = utils.toMultibase(id, 'base58btc'); 37 | const id_ = utils.fromMultibase(encoded); 38 | expect(id_).toBeDefined(); 39 | expect(Buffer.from(id).equals(Buffer.from(id_!))).toBe(true); 40 | }); 41 | test('ids can be encoded and decoded with uuid', () => { 42 | const idGen = new IdRandom(); 43 | const id = idGen.get(); 44 | const uuid = utils.toUUID(id); 45 | const id_ = utils.fromUUID(uuid); 46 | expect(id_).toBeDefined(); 47 | expect(Buffer.from(id).equals(Buffer.from(id_!))).toBe(true); 48 | }); 49 | test('ids are random', () => { 50 | const id = new IdRandom(); 51 | const count = 10; 52 | const ids = [...utils.take(id, count)].map((b) => 53 | Buffer.from(b).toString('hex'), 54 | ); 55 | const idSet = new Set(ids); 56 | expect(idSet.size).toBe(count); 57 | }); 58 | test('ids can be used as record indexes', () => { 59 | const idGen = new IdRandom(); 60 | const ids = [...utils.take(idGen, 10)]; 61 | let counter = 0; 62 | const record = {}; 63 | for (const id of ids) { 64 | record[id] = counter; 65 | expect(record[id]).toBe(counter); 66 | counter++; 67 | } 68 | }); 69 | test('ids in strings can be compared for equality', () => { 70 | const idGen = new IdRandom(); 71 | const id1 = idGen.get(); 72 | const id2 = idGen.get(); 73 | // Objects will be different 74 | expect(id1 == id2).toBe(false); // eslint-disable-line eqeqeq 75 | // Random ids are different 76 | expect(id1.toString() == id2.toString()).toBe(false); // eslint-disable-line eqeqeq 77 | expect(id1.toString()).toBe(id1 + ''); 78 | expect(id2.toString()).toBe(String(id2)); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/IdSortable.test.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '#index.js'; 2 | import { sleep, shuffle } from './utils.js'; 3 | import IdSortable, { extractTs, extractSeq, extractRand } from '#IdSortable.js'; 4 | import * as utils from '#utils.js'; 5 | 6 | describe('IdSortable', () => { 7 | test('ids are Uint8Array', () => { 8 | const idGen = new IdSortable(); 9 | const id = idGen.get(); 10 | expect(id).toBeInstanceOf(Uint8Array); 11 | }); 12 | test('ids can be generated', () => { 13 | const idGen = new IdSortable(); 14 | const ids = [...utils.take(idGen, 10)]; 15 | expect(ids).toHaveLength(10); 16 | }); 17 | test('ids can be generated as opaque type', () => { 18 | // Can't really check that types are working besides building 19 | // This is more of an example 20 | type Opaque = T & { __TYPE__: K }; 21 | type OpaqueId = Opaque<'opaque', Id>; 22 | const idGen = new IdSortable(); 23 | const ids = [...utils.take(idGen, 10)]; 24 | expect(ids).toHaveLength(10); 25 | }); 26 | test('ids can be encoded and decoded as binary strings', () => { 27 | const idGen = new IdSortable(); 28 | const id = idGen.get(); 29 | const encoded = id.toString(); 30 | const id_ = utils.fromString(encoded); 31 | expect(id_).toBeDefined(); 32 | expect(utils.toBuffer(id).equals(utils.toBuffer(id_!))).toBe(true); 33 | }); 34 | test('ids can be encoded and decoded with multibase', () => { 35 | const idGen = new IdSortable(); 36 | const id = idGen.get(); 37 | const encoded = utils.toMultibase(id, 'base58btc'); 38 | const id_ = utils.fromMultibase(encoded); 39 | expect(id_).toBeDefined(); 40 | expect(utils.toBuffer(id).equals(utils.toBuffer(id_!))).toBe(true); 41 | }); 42 | test('ids can be encoded and decoded with uuid', () => { 43 | const idGen = new IdSortable(); 44 | const id = idGen.get(); 45 | const uuid = utils.toUUID(id); 46 | const id_ = utils.fromUUID(uuid); 47 | expect(id_).toBeDefined(); 48 | expect(utils.toBuffer(id).equals(utils.toBuffer(id_!))).toBe(true); 49 | }); 50 | test('maintains the last id generated', () => { 51 | const idGen = new IdSortable(); 52 | idGen.get(); 53 | idGen.get(); 54 | const id = utils.toBuffer(idGen.get()); 55 | const id_ = utils.toBuffer(idGen.lastId); 56 | expect(id.equals(id_)).toBe(true); 57 | }); 58 | test('ids in bytes are lexically sortable', () => { 59 | const idGen = new IdSortable(); 60 | // This generating over 100,000 ids and checks that they maintain 61 | // sort order for each 100 chunk of ids 62 | let count = 1000; 63 | while (count > 0) { 64 | const idBuffers = [...utils.take(idGen, 100)]; 65 | const idBuffersShuffled = idBuffers.slice(); 66 | shuffle(idBuffersShuffled); 67 | // Comparison is done on the bytes in lexicographic order 68 | idBuffersShuffled.sort(Buffer.compare); 69 | expect(idBuffersShuffled).toStrictEqual(idBuffers); 70 | count--; 71 | } 72 | }); 73 | test('ids in bytes are lexically sortable with time delay', async () => { 74 | const id = new IdSortable(); 75 | const i1 = utils.toBuffer(id.get()); 76 | await sleep(250); 77 | const i2 = utils.toBuffer(id.get()); 78 | await sleep(500); 79 | const i3 = utils.toBuffer(id.get()); 80 | const buffers = [i3, i1, i2]; 81 | // Comparison is done on the bytes in lexicographic order 82 | buffers.sort(Buffer.compare); 83 | expect(buffers).toStrictEqual([i1, i2, i3]); 84 | }); 85 | test('encoded id strings are lexically sortable', () => { 86 | const idGen = new IdSortable(); 87 | // This generating over 100,000 ids and checks that they maintain 88 | // sort order for each 100 chunk of ids 89 | let count = 1000; 90 | while (count > 0) { 91 | const idStrings = [...utils.take(idGen, 100)].map((id) => id.toString()); 92 | const idStringsShuffled = idStrings.slice(); 93 | shuffle(idStringsShuffled); 94 | idStringsShuffled.sort(); 95 | expect(idStringsShuffled).toStrictEqual(idStrings); 96 | count--; 97 | } 98 | }); 99 | test('encoded uuids are lexically sortable', () => { 100 | // UUIDs are hex encoding, and the hex alphabet preserves order 101 | const idGen = new IdSortable(); 102 | // This generating over 100,000 ids and checks that they maintain 103 | // sort order for each 100 chunk of ids 104 | let count = 1000; 105 | while (count > 0) { 106 | const idUUIDs = [...utils.take(idGen, 100)].map(utils.toUUID); 107 | const idUUIDsShuffled = idUUIDs.slice(); 108 | shuffle(idUUIDsShuffled); 109 | idUUIDsShuffled.sort(); 110 | expect(idUUIDsShuffled).toStrictEqual(idUUIDs); 111 | count--; 112 | } 113 | }); 114 | test('encoded multibase strings may be lexically sortable (base32hex)', () => { 115 | // `base32hex` preserves sort order 116 | const idGen = new IdSortable(); 117 | // This generating over 100,000 ids and checks that they maintain 118 | // sort order for each 100 chunk of ids 119 | let count = 1000; 120 | while (count > 0) { 121 | const idStrings = [...utils.take(idGen, 100)].map((id) => 122 | utils.toMultibase(id, 'base32hex'), 123 | ); 124 | const idStringsShuffled = idStrings.slice(); 125 | shuffle(idStringsShuffled); 126 | idStringsShuffled.sort(); 127 | expect(idStringsShuffled).toStrictEqual(idStrings); 128 | count--; 129 | } 130 | }); 131 | test('encoded multibase strings may not be lexically sortable (base64)', async () => { 132 | // `base64` and `base58btc` does not preserve sort order 133 | const idGen = new IdSortable(); 134 | const idStrings = [...utils.take(idGen, 100)].map((id) => 135 | utils.toMultibase(id, 'base64'), 136 | ); 137 | const idStringsShuffled = idStrings.slice(); 138 | shuffle(idStringsShuffled); 139 | idStringsShuffled.sort(); 140 | // It will not equal 141 | expect(idStringsShuffled).not.toStrictEqual(idStrings); 142 | }); 143 | test('ids are monotonic within the same timestamp', () => { 144 | // To ensure that we it generates monotonic ids 145 | // we have to override the timesource 146 | const id = new IdSortable({ 147 | timeSource: () => { 148 | return () => 0; 149 | }, 150 | }); 151 | const i1 = utils.toBuffer(id.get()); 152 | const i2 = utils.toBuffer(id.get()); 153 | const i3 = utils.toBuffer(id.get()); 154 | // They should not equal 155 | expect(i1.equals(i2)).toBe(false); 156 | expect(i2.equals(i3)).toBe(false); 157 | const buffers = [i3, i1, i2]; 158 | // Comparison is done on the bytes in lexicographic order 159 | buffers.sort(Buffer.compare); 160 | expect(buffers).toStrictEqual([i1, i2, i3]); 161 | expect(extractTs(i1)).toBe(0); 162 | expect(extractTs(i2)).toBe(0); 163 | expect(extractTs(i3)).toBe(0); 164 | expect(extractSeq(i1)).toBe(0); 165 | expect(extractSeq(i2)).toBe(1); 166 | expect(extractSeq(i3)).toBe(2); 167 | }); 168 | test('ids are monotonic over process restarts', () => { 169 | const id = new IdSortable({ 170 | timeSource: () => { 171 | // 100 second in the future 172 | return () => Date.now() + 100000; 173 | }, 174 | }); 175 | const lastId = utils.toBuffer(id.get()); 176 | // Pass a future last id 177 | // the default time source should get an older timestamp 178 | const id_ = new IdSortable({ lastId }); 179 | const currId = utils.toBuffer(id_.get()); 180 | expect(lastId.equals(currId)).toBe(false); 181 | const buffers = [currId, lastId]; 182 | buffers.sort(Buffer.compare); 183 | expect(buffers).toStrictEqual([lastId, currId]); 184 | // Monotonicity is not enforced by seq 185 | // but rather the timestamp 186 | expect(extractSeq(lastId)).toBe(0); 187 | expect(extractSeq(currId)).toBe(0); 188 | }); 189 | test('ids can have machine id starting from the MSB of rand-section', () => { 190 | const nodeId = Buffer.from('abcd', 'utf-8'); 191 | const id = new IdSortable({ nodeId }); 192 | const i1 = utils.toBuffer(id.get()); 193 | const i2 = utils.toBuffer(id.get()); 194 | const i3 = utils.toBuffer(id.get()); 195 | const buffers = [i3, i1, i2]; 196 | // Comparison is done on the bytes in lexicographic order 197 | buffers.sort(Buffer.compare); 198 | expect(buffers).toStrictEqual([i1, i2, i3]); 199 | const randBits = extractRand(i1); 200 | expect(randBits.length).toBe(62); 201 | const randBytes = utils.bits2bytes(randBits); 202 | const nodeBytes = randBytes.slice(0, 4); 203 | expect(utils.toBuffer(nodeBytes).equals(nodeId)).toBe(true); 204 | }); 205 | test('ids can be used as record indexes', () => { 206 | const idGen = new IdSortable(); 207 | const ids = [...utils.take(idGen, 10)]; 208 | let counter = 0; 209 | const record = {}; 210 | for (const id of ids) { 211 | record[id] = counter; 212 | expect(record[id]).toBe(counter); 213 | counter++; 214 | } 215 | }); 216 | test('ids can use comparison operators', () => { 217 | const idGen = new IdSortable(); 218 | let idToCompare = idGen.get(); 219 | const ids = [...utils.take(idGen, 100)]; 220 | for (const id of ids) { 221 | expect(idToCompare < id).toBe(true); 222 | expect(idToCompare <= id).toBe(true); 223 | expect(idToCompare > id).toBe(false); 224 | expect(idToCompare >= id).toBe(false); 225 | idToCompare = id; 226 | } 227 | }); 228 | test('ids in strings can be compared for equality', () => { 229 | const idGen = new IdSortable(); 230 | const id1 = idGen.get(); 231 | const id2 = idGen.get(); 232 | // Objects will be different 233 | expect(id1 == id2).toBe(false); // eslint-disable-line eqeqeq 234 | // Sortable ids are different 235 | expect(id1.toString() == id2.toString()).toBe(false); // eslint-disable-line eqeqeq 236 | expect(id1.toString()).toBe(id1 + ''); 237 | expect(id2.toString()).toBe(String(id2)); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /tests/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | 3 | /// 4 | 5 | /** 6 | * Follows the globals in jest.config.ts 7 | * @module 8 | */ 9 | declare var projectDir: string; 10 | declare var testDir: string; 11 | declare var defaultTimeout: number; 12 | declare var maxTimeout: number; 13 | -------------------------------------------------------------------------------- /tests/globalSetup.ts: -------------------------------------------------------------------------------- 1 | async function setup() { 2 | // eslint-disable-next-line no-console 3 | console.log('\nGLOBAL SETUP'); 4 | } 5 | 6 | export default setup; 7 | -------------------------------------------------------------------------------- /tests/globalTeardown.ts: -------------------------------------------------------------------------------- 1 | async function teardown() { 2 | // eslint-disable-next-line no-console 3 | console.log('GLOBAL TEARDOWN'); 4 | } 5 | 6 | export default teardown; 7 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-id/9654b7fd0c75e31f357af6cbb7a8af849974503f/tests/setup.ts -------------------------------------------------------------------------------- /tests/setupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | 3 | // Default timeout per test 4 | // some tests may take longer in which case you should specify the timeout 5 | // explicitly for each test by using the third parameter of test function 6 | jest.setTimeout(globalThis.defaultTimeout); 7 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | import * as utils from '#utils.js'; 3 | 4 | describe('utils', () => { 5 | test('take from an iterator', () => { 6 | // Arrays are not iterators, but you can acquire the iterator 7 | expect([...utils.take([1, 2, 3, 4][Symbol.iterator](), 2)]).toStrictEqual([ 8 | 1, 2, 9 | ]); 10 | }); 11 | test('encoding & decoding UUID', () => { 12 | const hex1 = '01858a9e0e5c73edbde194f017ebdb3b'; 13 | const id1 = '01858a9e-0e5c-73ed-bde1-94f017ebdb3b'; 14 | const bytes1 = utils.hex2bytes(hex1); 15 | const uuid1 = utils.toUUID(bytes1); 16 | expect(uuid1).toBe(id1); 17 | // Use uuid library to confirm 18 | const bytes2 = uuid.v4({}, new Uint8Array(16)); 19 | const id2 = uuid.stringify([...bytes2]); 20 | expect(utils.toUUID(bytes2)).toBe(id2); 21 | const bytes1_ = utils.fromUUID(uuid1); 22 | expect(Buffer.from(bytes1_!).equals(bytes1)).toBe(true); 23 | // Cannot convert to UUID if the byte length is not 16 bytes 24 | const hex3 = '01858a9e0e5c73edbde194f017ebdb3b0185'; 25 | const bytes3 = utils.hex2bytes(hex3); 26 | expect(() => { 27 | utils.toUUID(bytes3); 28 | }).toThrow(RangeError); 29 | const hex4 = '0185'; 30 | const bytes4 = utils.hex2bytes(hex4); 31 | expect(() => { 32 | utils.toUUID(bytes4); 33 | }).toThrow(RangeError); 34 | }); 35 | test('encoding and decoding bytes and bit strings', () => { 36 | // 128 size bit string 37 | const bits = 38 | '00000110000101100010100100001100101001110100010001110000000000001011000111101000111001101100100010110010011110110110110100110011'; 39 | const bytes = new Uint8Array([ 40 | 6, 22, 41, 12, 167, 68, 112, 0, 177, 232, 230, 200, 178, 123, 109, 51, 41 | ]); 42 | expect(utils.bits2bytes(bits)).toStrictEqual(bytes); 43 | expect(utils.bytes2bits(bytes)).toBe(bits); 44 | expect(utils.bytes2bits(utils.bits2bytes(bits))).toBe(bits); 45 | expect(utils.bits2bytes(utils.bytes2bits(bytes))).toStrictEqual(bytes); 46 | }); 47 | test('encoding and decoding bytes and hex strings', () => { 48 | // Uuid hex 49 | const hex = '0616290ca7447000b1e8e6c8b27b6d33'; 50 | const bytes = new Uint8Array([ 51 | 6, 22, 41, 12, 167, 68, 112, 0, 177, 232, 230, 200, 178, 123, 109, 51, 52 | ]); 53 | expect(utils.hex2bytes(hex)).toStrictEqual(bytes); 54 | expect(utils.bytes2hex(bytes)).toBe(hex); 55 | expect(utils.bytes2hex(utils.hex2bytes(hex))).toBe(hex); 56 | expect(utils.hex2bytes(utils.bytes2hex(bytes))).toStrictEqual(bytes); 57 | }); 58 | test('encoding decimal to bit strings', () => { 59 | expect(utils.dec2bits(0, 8)).toBe('00000000'); 60 | expect(utils.dec2bits(1, 8)).toBe('00000001'); 61 | expect(utils.dec2bits(2, 8)).toBe('00000010'); 62 | expect(utils.dec2bits(255, 8)).toBe('11111111'); 63 | // This should roll back to the beginning 64 | expect(utils.dec2bits(256, 8)).toBe('00000000'); 65 | expect(utils.dec2bits(257, 8)).toBe('00000001'); 66 | }); 67 | test('encoding decimal to hex strings', () => { 68 | expect(utils.dec2hex(0, 2)).toBe('00'); 69 | expect(utils.dec2hex(1, 2)).toBe('01'); 70 | expect(utils.dec2hex(2, 2)).toBe('02'); 71 | expect(utils.dec2hex(10, 2)).toBe('0a'); 72 | expect(utils.dec2hex(15, 2)).toBe('0f'); 73 | expect(utils.dec2hex(255, 2)).toBe('ff'); 74 | // This should roll back to the beginning 75 | expect(utils.dec2hex(256, 2)).toBe('00'); 76 | expect(utils.dec2hex(257, 2)).toBe('01'); 77 | }); 78 | test('chunking strings', () => { 79 | const s1 = '111222333'; 80 | const c1 = utils.strChunks(s1, 3); 81 | expect(c1).toStrictEqual(['111', '222', '333']); 82 | const s2 = '11122233'; 83 | const c2 = utils.strChunks(s2, 3); 84 | expect(c2).toStrictEqual(['111', '222', '33']); 85 | }); 86 | test('rounding to decimal precision', () => { 87 | expect(utils.roundPrecise(0.1234, 3)).toBe(0.123); 88 | expect(utils.roundPrecise(1.26, 1)).toBe(1.3); 89 | expect(utils.roundPrecise(1.31, 1)).toBe(1.3); 90 | expect(utils.roundPrecise(1.255, 2)).toBe(1.26); 91 | expect(utils.roundPrecise(1.5)).toBe(2); 92 | expect(utils.roundPrecise(1.49)).toBe(1); 93 | }); 94 | test('fixed point conversion', () => { 95 | // To 3 decimal places 96 | // we should expect .102 to be the resulting fractional 97 | // eslint-disable-next-line no-loss-of-precision 98 | const fp1 = 1633860855.1015312; 99 | const fixed1 = utils.toFixedPoint(fp1, 12, 3); 100 | expect(fixed1[1]).toBe(417); 101 | const fp1_ = utils.fromFixedPoint(fixed1, 12, 3); 102 | expect(fp1_).toBe(utils.roundPrecise(fp1, 3)); 103 | // Also to 3 decimal places 104 | // expecting 0.101 now 105 | // eslint-disable-next-line no-loss-of-precision 106 | const fp2 = 1633860855.1014312; 107 | const fixed2 = utils.toFixedPoint(fp2, 12, 3); 108 | expect(fixed2[1]).toBe(413); 109 | const fp2_ = utils.fromFixedPoint(fixed2, 12, 3); 110 | expect(fp2_).toBe(utils.roundPrecise(fp2, 3)); 111 | // 0 edge case 112 | expect(utils.toFixedPoint(0, 12, 3)).toStrictEqual([0, 0]); 113 | expect(utils.fromFixedPoint([0, 0], 12, 3)).toBe(0.0); 114 | }); 115 | test('fixed point conversion when close to 1', () => { 116 | // Highest number 12 digits can represent is 4095 117 | // a number of 4096 would result in overflow 118 | // here we test when we get close to 4096 119 | // the conversion from toFixedPoint and fromFixedPoint will be lossy 120 | // this is because toFixedPoint will round off precision and floor any number getting close to 4096 121 | let closeTo: number; 122 | let fp: [number, number]; 123 | // Exactly at 3 decimal points 124 | closeTo = 0.999; 125 | fp = utils.toFixedPoint(closeTo, 12, 3); 126 | expect(fp).toStrictEqual([0, 4091]); 127 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.999); 128 | // Will round below 129 | closeTo = 0.9994; 130 | fp = utils.toFixedPoint(closeTo, 12, 3); 131 | expect(fp).toStrictEqual([0, 4091]); 132 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.999); 133 | // Will round above to 1 134 | closeTo = 0.9995; 135 | fp = utils.toFixedPoint(closeTo, 12, 3); 136 | expect(fp).toStrictEqual([1, 0]); 137 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(1); 138 | // Will round above to 1 139 | closeTo = 0.9999; 140 | fp = utils.toFixedPoint(closeTo, 12, 3); 141 | expect(fp).toStrictEqual([1, 0]); 142 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(1); 143 | // Will round above to 1 144 | closeTo = 0.99999; 145 | fp = utils.toFixedPoint(closeTo, 12, 3); 146 | expect(fp).toStrictEqual([1, 0]); 147 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(1); 148 | // Exactly at 5 decimal points 149 | closeTo = 0.99999; 150 | fp = utils.toFixedPoint(closeTo, 12, 5); 151 | expect(fp).toStrictEqual([0, 4095]); 152 | expect(utils.fromFixedPoint(fp, 12, 5)).toBe(0.99976); 153 | }); 154 | test('fixed point conversion when close to 0', () => { 155 | let closeTo: number; 156 | let fp: [number, number]; 157 | // Exactly 3 decimal places 158 | closeTo = 0.001; 159 | fp = utils.toFixedPoint(closeTo, 12, 3); 160 | expect(fp).toStrictEqual([0, 4]); 161 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.001); 162 | // Will round to 0 163 | closeTo = 0.0001; 164 | fp = utils.toFixedPoint(closeTo, 12, 3); 165 | expect(fp).toStrictEqual([0, 0]); 166 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0); 167 | // Will round to 0 168 | closeTo = 0.0004; 169 | fp = utils.toFixedPoint(closeTo, 12, 3); 170 | expect(fp).toStrictEqual([0, 0]); 171 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0); 172 | // Will round to 0.001 173 | closeTo = 0.0005; 174 | fp = utils.toFixedPoint(closeTo, 12, 3); 175 | expect(fp).toStrictEqual([0, 4]); 176 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.001); 177 | // Will round to 0.001 178 | closeTo = 0.00055; 179 | fp = utils.toFixedPoint(closeTo, 12, 3); 180 | expect(fp).toStrictEqual([0, 4]); 181 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.001); 182 | // Will round to 0.001 183 | closeTo = 0.0009; 184 | fp = utils.toFixedPoint(closeTo, 12, 3); 185 | expect(fp).toStrictEqual([0, 4]); 186 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.001); 187 | // Will round to 0.002 188 | closeTo = 0.0015; 189 | fp = utils.toFixedPoint(closeTo, 12, 3); 190 | expect(fp).toStrictEqual([0, 8]); 191 | expect(utils.fromFixedPoint(fp, 12, 3)).toBe(0.002); 192 | }); 193 | test('multibase encoding and decoding', () => { 194 | const bytes = new Uint8Array([ 195 | 123, 124, 125, 126, 127, 128, 129, 130, 123, 124, 125, 126, 127, 128, 129, 196 | 130, 197 | ]); 198 | const encoded = utils.toMultibase(bytes, 'base58btc'); 199 | expect(encoded).toBe('zGFRLUyEszBgw9bRXTeFvu7'); 200 | const bytes_ = utils.fromMultibase(encoded); 201 | expect(bytes_).toBeDefined(); 202 | expect(Buffer.from(bytes_!).equals(Buffer.from(bytes))).toBe(true); 203 | // FromMultibase should only allow 16 byte ids 204 | expect(utils.fromMultibase('aAQ3')).toBeUndefined(); 205 | expect(utils.fromMultibase('aAQ333333333333333AAAAAA')).toBeUndefined(); 206 | expect( 207 | utils.fromMultibase('zF4VfF3uRhSqgxTOOLONGxTRdVKauV9'), 208 | ).toBeUndefined(); 209 | expect(utils.fromMultibase('zFxTOOSHORTx9')).toBeUndefined(); 210 | expect(utils.fromMultibase('helloworld')).toBeUndefined(); 211 | }); 212 | test('buffer encoding and decoding is zero copy', () => { 213 | const bytes = new Uint8Array([ 214 | 123, 124, 125, 126, 127, 128, 129, 130, 123, 124, 125, 126, 127, 128, 129, 215 | 130, 216 | ]); 217 | const buffer = utils.toBuffer(bytes); 218 | buffer[0] = 122; 219 | expect(bytes[0]).toBe(122); 220 | const bytes_ = utils.fromBuffer(buffer); 221 | expect(bytes_).toBeDefined(); 222 | bytes_![0] = 121; 223 | expect(bytes[0]).toBe(121); 224 | expect(buffer[0]).toBe(121); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | async function sleep(ms: number) { 2 | return await new Promise((r) => setTimeout(r, ms)); 3 | } 4 | 5 | function shuffle(arr: Array): void { 6 | for (let i = arr.length - 1; i > 0; i--) { 7 | const j = Math.floor(Math.random() * (i + 1)); 8 | [arr[i], arr[j]] = [arr[j], arr[i]]; 9 | } 10 | } 11 | 12 | export { sleep, shuffle }; 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "noEmit": false, 6 | "stripInternal": true 7 | }, 8 | "exclude": [ 9 | "./tests/**/*", 10 | "./scripts/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "tsBuildInfoFile": "./dist/tsbuildinfo", 5 | "incremental": true, 6 | "sourceMap": true, 7 | "declaration": true, 8 | "allowJs": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": false, 11 | "experimentalDecorators": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleResolution": "NodeNext", 17 | "module": "ESNext", 18 | "target": "ES2022", 19 | "baseUrl": "./src", 20 | "paths": { 21 | "#*": ["*"] 22 | }, 23 | "skipLibCheck": true, 24 | "noEmit": true 25 | }, 26 | "include": [ 27 | "./src/**/*", 28 | "./src/**/*.json", 29 | "./tests/**/*", 30 | "./scripts/**/*" 31 | ], 32 | "ts-node": { 33 | "esm": true, 34 | "transpileOnly": true, 35 | "swc": true 36 | } 37 | } 38 | --------------------------------------------------------------------------------