├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── node.js.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── esbuild.js ├── expirables-logo.png ├── expirables.png ├── package-lock.json ├── package.json ├── packages └── website │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── contributors.js │ ├── conventional_commit_config.js │ ├── docs │ ├── intro.md │ ├── linked-list.md │ ├── map.md │ ├── queue.md │ ├── set.md │ └── stack.md │ ├── docusaurus.config.js │ ├── package-lock.json │ ├── package.json │ ├── sidebars.js │ ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx │ ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus-social-card.png │ │ ├── docusaurus.png │ │ ├── expirables-logo.png │ │ ├── expirables.png │ │ ├── favicon.ico │ │ ├── undraw_delivery_truck_vt6p.svg │ │ ├── undraw_open_source_-1-qxw.svg │ │ └── undraw_task_re_wi3v.svg │ └── tsconfig.json ├── src ├── LinkedList │ ├── Node.ts │ └── index.ts ├── Map │ └── index.ts ├── Queue │ └── index.ts ├── Set │ └── index.ts ├── Stack │ └── index.ts ├── index.ts ├── types │ └── index.ts └── utils │ ├── hooks.ts │ ├── index.ts │ ├── timers.ts │ └── ttl.ts ├── test ├── linked-list.test.ts ├── map.test.ts ├── queue.test.ts ├── set.test.ts └── stack.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | demo 4 | esbuild.js 5 | coverage 6 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "ignorePatterns": ["node_modules/", "dist/", "commitlint.config.js"], 11 | "rules": { 12 | "no-console": 2, 13 | "no-debugger": 2, 14 | "no-var": 2, 15 | "no-unused-vars": 0, 16 | "@typescript-eslint/no-explicit-any": 0, 17 | "@typescript-eslint/ban-types": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: ncipollo/release-action@v1 16 | with: 17 | generateReleaseNotes: true 18 | makeLatest: true 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | lerna-debug.log* 3 | 4 | node_modules 5 | dist 6 | 7 | .vscode/* 8 | !.vscode/extensions.json 9 | .idea 10 | .DS_Store 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | *.sw? 16 | 17 | coverage -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run check 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | demo 4 | coverage -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | demo -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 1.7.1 (2024-05-24) 2 | 3 | ##### Chores 4 | 5 | * fixing package json ([bc6d9005](https://github.com/Cadienvan/expirables/commit/bc6d90052d47947ffe76a685b642b6c83ef4a3fc)) 6 | * **deps:** 7 | * bump express from 4.18.2 to 4.19.2 in /packages/website ([#49](https://github.com/Cadienvan/expirables/pull/49)) ([33127936](https://github.com/Cadienvan/expirables/commit/33127936e8767e8af77a5875a9c491d856022f75)) 8 | * bump webpack-dev-middleware in /packages/website ([#48](https://github.com/Cadienvan/expirables/pull/48)) ([b0a31fd0](https://github.com/Cadienvan/expirables/commit/b0a31fd0af1a9e14411de11568ac67e51fd9cba6)) 9 | 10 | ##### Documentation Changes 11 | 12 | * updated changelog ([cccc31df](https://github.com/Cadienvan/expirables/commit/cccc31df926c265f72d41050f53045f0b940a065)) 13 | 14 | #### 1.7.1 (2024-05-24) 15 | 16 | ##### Chores 17 | 18 | * **deps:** 19 | * bump express from 4.18.2 to 4.19.2 in /packages/website ([#49](https://github.com/Cadienvan/expirables/pull/49)) ([33127936](https://github.com/Cadienvan/expirables/commit/33127936e8767e8af77a5875a9c491d856022f75)) 20 | * bump webpack-dev-middleware in /packages/website ([#48](https://github.com/Cadienvan/expirables/pull/48)) ([b0a31fd0](https://github.com/Cadienvan/expirables/commit/b0a31fd0af1a9e14411de11568ac67e51fd9cba6)) 21 | 22 | ### 1.7.0 (2024-03-17) 23 | 24 | ##### Chores 25 | 26 | * **deps:** 27 | * bump follow-redirects in /packages/website ([#47](https://github.com/Cadienvan/expirables/pull/47)) ([600a4c4c](https://github.com/Cadienvan/expirables/commit/600a4c4cb007dd19b4349e0f5fa6968600e1c3aa)) 28 | * bump follow-redirects in /packages/website ([#41](https://github.com/Cadienvan/expirables/pull/41)) ([3609e064](https://github.com/Cadienvan/expirables/commit/3609e0640264ca00a37a28fb8373a76447f7f615)) 29 | 30 | ##### Bug Fixes 31 | 32 | * upgrade @docusaurus/core from 3.0.1 to 3.1.0 ([#42](https://github.com/Cadienvan/expirables/pull/42)) ([c56a5395](https://github.com/Cadienvan/expirables/commit/c56a5395cc5465d12446542eeefe3df4051abf54)) 33 | 34 | #### 1.6.8 (2023-12-27) 35 | 36 | ##### Continuous Integration 37 | 38 | * removing node 14 and 16 from ci ([d4444f5a](https://github.com/Cadienvan/expirables/commit/d4444f5ada663e99fdcf4f2bbc0479020c402173)) 39 | 40 | ##### New Features 41 | 42 | * updating docs to docusaurus 3 ([90dcfd6a](https://github.com/Cadienvan/expirables/commit/90dcfd6ad855d8b878a86bce6d848364991425a0)) 43 | 44 | ##### Refactors 45 | 46 | * **test:** remove tap from expirables ([#40](https://github.com/Cadienvan/expirables/pull/40)) ([cfcdf83a](https://github.com/Cadienvan/expirables/commit/cfcdf83a225f6be1f42cf6003f9699ef327666e4)) 47 | 48 | #### 1.6.7 (2023-10-18) 49 | 50 | ##### Chores 51 | 52 | * **deps:** 53 | * bump @babel/traverse in /packages/website ([#33](https://github.com/Cadienvan/expirables/pull/33)) ([578f3813](https://github.com/Cadienvan/expirables/commit/578f38130f3d413901b0977448610fe4a1efaa7c)) 54 | * bump @babel/traverse ([#32](https://github.com/Cadienvan/expirables/pull/32)) ([d6a49989](https://github.com/Cadienvan/expirables/commit/d6a4998957b98b94dc5b76a178374c3f38592e3a)) 55 | 56 | #### 1.6.6 (2023-10-08) 57 | 58 | ##### Chores 59 | 60 | * fixing type emission on docusaurus ([639eb121](https://github.com/Cadienvan/expirables/commit/639eb12166ed0144483448102cc7fa2cc1c5fa40)) 61 | * **deps:** bump postcss from 8.4.27 to 8.4.31 in /packages/website ([#31](https://github.com/Cadienvan/expirables/pull/31)) ([0fc37e1e](https://github.com/Cadienvan/expirables/commit/0fc37e1e02f597e1bbc5f079368bd7bdda7996c0)) 62 | 63 | ##### New Features 64 | 65 | * added prebuild hook ([18c650a2](https://github.com/Cadienvan/expirables/commit/18c650a2ca3660acaf78e92a034b0464d57558ec)) 66 | 67 | #### 1.6.5 (2023-07-20) 68 | 69 | ##### Chores 70 | 71 | * **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 ([#26](https://github.com/Cadienvan/expirables/pull/26)) ([ade0a2ff](https://github.com/Cadienvan/expirables/commit/ade0a2ff7366fa2dd6e00e7af84b1e6869d1229b)) 72 | 73 | #### 1.6.4 (2023-05-30) 74 | 75 | ##### Chores 76 | 77 | * added logo ([19572127](https://github.com/Cadienvan/expirables/commit/19572127caa4a96fe5161038db07a57bc7dbaa41)) 78 | * removed logo ([0d05df35](https://github.com/Cadienvan/expirables/commit/0d05df3561c3c9a25dd40166a44ac5047189de84)) 79 | * added logo ([8f179afe](https://github.com/Cadienvan/expirables/commit/8f179afe7655ebed65d3aeae2e785e4f4ff99c64)) 80 | 81 | ##### New Features 82 | 83 | * add Node v20 compatibility ([#25](https://github.com/Cadienvan/expirables/pull/25)) ([3cc4d90c](https://github.com/Cadienvan/expirables/commit/3cc4d90cd0386f44e1954b5c71871d604f225701)) 84 | 85 | #### 1.6.3 (2023-04-26) 86 | 87 | #### 1.6.2 (2023-04-26) 88 | 89 | ##### Chores 90 | 91 | * **deps:** bump yaml from 2.1.3 to 2.2.2 ([#21](https://github.com/Cadienvan/expirables/pull/21)) ([5161f01d](https://github.com/Cadienvan/expirables/commit/5161f01df9c7ed267be62946531da555fceedfc9)) 92 | 93 | #### 1.6.2 (2023-04-26) 94 | 95 | ##### Chores 96 | 97 | * **deps:** bump yaml from 2.1.3 to 2.2.2 ([#21](https://github.com/Cadienvan/expirables/pull/21)) ([5161f01d](https://github.com/Cadienvan/expirables/commit/5161f01df9c7ed267be62946531da555fceedfc9)) 98 | 99 | #### 1.6.1 (2023-03-21) 100 | 101 | ##### New Features 102 | 103 | * release action ([#19](https://github.com/Cadienvan/expirables/pull/19)) ([019edc36](https://github.com/Cadienvan/expirables/commit/019edc36e35703ba25fa12453b196f2e4feda4b0)) 104 | 105 | ##### Refactors 106 | 107 | * **tests:** replace jest with tap ([#20](https://github.com/Cadienvan/expirables/pull/20)) ([719beda7](https://github.com/Cadienvan/expirables/commit/719beda7406fe9bc4aa7c6e569c23a108512a20d)) 108 | 109 | ### 1.6.0 (2023-03-14) 110 | 111 | ##### Chores 112 | 113 | * **LinkedList:** linting docs ([d113d2a0](https://github.com/Cadienvan/expirables/commit/d113d2a08448761e9e3845b679427d27fcf5b654)) 114 | 115 | ##### Documentation Changes 116 | 117 | * **LinkedList:** improved docs adding missing API ([6270c255](https://github.com/Cadienvan/expirables/commit/6270c2550b93edce5378f69f276b22a6b9e0e3d2)) 118 | 119 | ##### New Features 120 | 121 | * moved setExpiration checks away from timeouts to improve perfs + added some checks on time ([8391836e](https://github.com/Cadienvan/expirables/commit/8391836e1e2285d1835da4098fd4b03dbdcec854)) 122 | * added hooks to LinkedList + test + docs + get method ([#15](https://github.com/Cadienvan/expirables/pull/15)) ([1e652d71](https://github.com/Cadienvan/expirables/commit/1e652d718f53487274d3af661ccbe6e91ed06d19)) 123 | * added hooks to stack + test + doc ([#14](https://github.com/Cadienvan/expirables/pull/14)) ([c0a7fcbc](https://github.com/Cadienvan/expirables/commit/c0a7fcbc7b90d9dbc75c7c126dff8072b94a828c)) 124 | * implemented hooks system ([#12](https://github.com/Cadienvan/expirables/pull/12)) ([f058e7bc](https://github.com/Cadienvan/expirables/commit/f058e7bcbf87e536f8c9de4d949e87696d31f2cb)) 125 | * **map:** implemented hooks + test + docs ([#18](https://github.com/Cadienvan/expirables/pull/18)) ([1d7e3de7](https://github.com/Cadienvan/expirables/commit/1d7e3de773b7069c3788836c1480856b5892a5ff)) 126 | * **set:** implemented hooks + tests + docs ([#16](https://github.com/Cadienvan/expirables/pull/16)) ([732d0ec2](https://github.com/Cadienvan/expirables/commit/732d0ec24c2533c88cc8984847ceab3dcd0bbbe5)) 127 | 128 | ##### Bug Fixes 129 | 130 | * **timers:** flaky test ([#10](https://github.com/Cadienvan/expirables/pull/10)) ([834da36a](https://github.com/Cadienvan/expirables/commit/834da36a27cf0ff102bec1f6b371403c3a844b99)) 131 | 132 | ### 1.5.0 (2023-03-11) 133 | 134 | ##### Chores 135 | 136 | * export linked list ([#6](https://github.com/Cadienvan/expirables/pull/6)) ([ef374615](https://github.com/Cadienvan/expirables/commit/ef37461570936c2f64bdea53ba0edbddba2136f9)) 137 | 138 | ##### Performance Improvements 139 | 140 | * **husky:** prepare script and command ([#8](https://github.com/Cadienvan/expirables/pull/8)) ([f7805d2d](https://github.com/Cadienvan/expirables/commit/f7805d2d04a4f1e9f18762906426aac9c014124c)) 141 | * improve reusable types ([#7](https://github.com/Cadienvan/expirables/pull/7)) ([fff15063](https://github.com/Cadienvan/expirables/commit/fff150635488496b5ac5cce5222c4c050ba945a6)) 142 | 143 | #### 1.4.1 (2023-01-24) 144 | 145 | ##### Tests 146 | 147 | * fixed flacky test ([178d8bad](https://github.com/Cadienvan/expirables/commit/178d8bad98f87b18805c5f2f0cfc69fe627aba47)) 148 | 149 | ### 1.4.0 (2023-01-24) 150 | 151 | ##### Chores 152 | 153 | * **deps:** bump json5 from 2.2.1 to 2.2.3 ([#2](https://github.com/Cadienvan/expirables/pull/2)) ([fed3bc1a](https://github.com/Cadienvan/expirables/commit/fed3bc1ab1dd45c2ed44cf25e4d362c8ee3a63cc)) 154 | 155 | ##### Documentation Changes 156 | 157 | * updated docs to accomodate ([c8c15c86](https://github.com/Cadienvan/expirables/commit/c8c15c8655be1cb1ea916aff62359a4c3865e07b)) 158 | 159 | ##### New Features 160 | 161 | * added Linked List ([21bc49c8](https://github.com/Cadienvan/expirables/commit/21bc49c8a900fa0561dabd6a8f3d9eeb81f77b08)) 162 | 163 | ##### Tests 164 | 165 | * improved coverage to 100% lines ([#4](https://github.com/Cadienvan/expirables/pull/4)) ([3ae3daa9](https://github.com/Cadienvan/expirables/commit/3ae3daa98200473b3641cd4b308048197a3c000a)) 166 | 167 | ### 1.3.0 (2022-12-17) 168 | 169 | ##### Chores 170 | 171 | * updated package lock ([c5c7e90a](https://github.com/Cadienvan/expirables/commit/c5c7e90abd9a72eb31918e875b5562288297583b)) 172 | * brought back previous version and changelog for compatibility ([1fe1e395](https://github.com/Cadienvan/expirables/commit/1fe1e39543a2d9befed323ae6c65ad3437e83b62)) 173 | 174 | ##### Documentation Changes 175 | 176 | * updated changelog ([18bcd1fa](https://github.com/Cadienvan/expirables/commit/18bcd1fa66280273d75fb329c7bd95047d57b6c6)) 177 | * updated changelog ([8a6e8e02](https://github.com/Cadienvan/expirables/commit/8a6e8e02fa424aa444596221db36b8c6b432eb82)) 178 | 179 | ##### Other Changes 180 | 181 | * package name changed + added missing export ([6456dd18](https://github.com/Cadienvan/expirables/commit/6456dd186e30c8459b06dea8ff230d65cc2ad8d6)) 182 | 183 | ### 1.3.0 (2022-12-17) 184 | 185 | ##### Chores 186 | 187 | * brought back previous version and changelog for compatibility ([1fe1e395](https://github.com/Cadienvan/expirables/commit/1fe1e39543a2d9befed323ae6c65ad3437e83b62)) 188 | 189 | ##### Documentation Changes 190 | 191 | * updated changelog ([8a6e8e02](https://github.com/Cadienvan/expirables/commit/8a6e8e02fa424aa444596221db36b8c6b432eb82)) 192 | 193 | ##### Other Changes 194 | 195 | * package name changed + added missing export ([6456dd18](https://github.com/Cadienvan/expirables/commit/6456dd186e30c8459b06dea8ff230d65cc2ad8d6)) 196 | 197 | ### 1.3.0 (2022-12-17) 198 | 199 | ##### Chores 200 | 201 | * brought back previous version and changelog for compatibility ([1fe1e395](https://github.com/Cadienvan/expirables/commit/1fe1e39543a2d9befed323ae6c65ad3437e83b62)) 202 | 203 | ##### Other Changes 204 | 205 | * package name changed + added missing export ([6456dd18](https://github.com/Cadienvan/expirables/commit/6456dd186e30c8459b06dea8ff230d65cc2ad8d6)) 206 | 207 | #### 1.2.2 (2022-12-12) 208 | 209 | ##### Chores 210 | 211 | * added automatic changelog generation ([1804d6e3](https://github.com/Cadienvan/expirables/commit/1804d6e3519bf8f4cd64cf0c81643f281ea76f2f)) 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 cadienvan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Expirables Logo](./expirables.png) 2 | 3 | # What is this? 4 | 5 | This is a zero dependency package that provides some expirable implementations of common Data Structures. 6 | Thanks to the [npm-package-ts-scaffolding](https://github.com/Cadienvan/npm-package-ts-scaffolding) it is importable both via CommonJS and ES Modules. 7 | It currently supports the following Data Structures: 8 | 9 | - [ExpirableMap](./packages/website/docs/map.md) 10 | - [ExpirableSet](./packages/website/docs/set.md) 11 | - [ExpirableQueue](./packages/website/docs/queue.md) 12 | - [ExpirableStack](./packages/website/docs/stack.md) 13 | - [ExpirableLinkedList](./packages/website/docs/linked-list.md) 14 | 15 | # How do I install it? 16 | 17 | You can install it by using the following command: 18 | 19 | ```bash 20 | npm install expirables 21 | ``` 22 | 23 | # Tests 24 | 25 | You can run the tests by using the following command: 26 | 27 | ```bash 28 | npm test 29 | ``` 30 | 31 | # Scaffolding 32 | 33 | This project was generated using Cadienvan's own [npm-package-ts-scaffolding](https://github.com/Cadienvan/npm-package-ts-scaffolding) so it has all the necessary tools to develop, test and publish a TypeScript package importable both via CommonJS and ES Modules. 34 | 35 | # FAQ 36 | 37 | ## Why are you using timeouts instead of lazy evaluation? 38 | 39 | - Lazy evaluation would need to re-implement many methods of the Data Structures and would be much more complex to implement and maintain. 40 | - Lazy evaluation could block the main thread for a long time if the Data Structure is big, and moving it to a Worker would be a lot of work for a small gain. 41 | - Lazy evaluation would require a higher amount of memory to work because it would store all the expired entries until they are evaluated. 42 | - Lazy evaluation would need us to store additional information about the entries (e.g. the expiration time) which would increase the memory footprint of the Data Structure. 43 | 44 | # Contributing 45 | 46 | If you want to contribute to this project, please open an issue or a pull request. -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | const __DEV__ = process.env.NODE_ENV === 'development'; 4 | const __PROD__ = process.env.NODE_ENV === 'production'; 5 | 6 | // ESM - Currently disabled as CommonJS named exports seem to work pretty well with ESM based imports 7 | /*esbuild 8 | .build({ 9 | entryPoints: ['src/index.ts'], 10 | outdir: 'dist', 11 | bundle: true, 12 | sourcemap: true, 13 | minify: true, 14 | splitting: true, 15 | format: 'esm', 16 | target: ['esnext'] 17 | }) 18 | .catch(() => process.exit(1));*/ 19 | 20 | // CJS 21 | esbuild 22 | .build({ 23 | entryPoints: ['src/index.ts'], 24 | outfile: 'dist/index.js', 25 | format: 'cjs', 26 | bundle: true, 27 | sourcemap: __DEV__, 28 | minify: __PROD__, 29 | platform: 'node', 30 | // ???? 31 | // there's no node14.X option https://esbuild.github.io/api/#target 32 | target: ['node14.16'] 33 | }) 34 | .catch(() => process.exit(1)); 35 | -------------------------------------------------------------------------------- /expirables-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/expirables-logo.png -------------------------------------------------------------------------------- /expirables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/expirables.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expirables", 3 | "description": "This is a zero dependency package that provides some expirable implementations of common Data Structures.", 4 | "private": false, 5 | "version": "1.7.1", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "types": "dist/src/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js", 15 | "require": "./dist/index.js" 16 | }, 17 | "./package.json": "./package.json" 18 | }, 19 | "scripts": { 20 | "ts-types": "tsc --emitDeclarationOnly --outDir dist", 21 | "check": "npm run prettier && npm run lint && npm test", 22 | "build": "npm run check && rimraf dist && NODE_ENV=production node esbuild.js && npm run ts-types", 23 | "prettier": "prettier --write ./src", 24 | "lint": "eslint ./src --ext .ts", 25 | "prepare": "husky install", 26 | "test": "c8 node --import tsx --test ./test/*.test.*", 27 | "release:common": "npm run build && git push --follow-tags origin main && npm publish --access public", 28 | "release:patch": "changelog -p && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version patch && npm run release:common", 29 | "release:minor": "changelog -m && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version minor && npm run release:common", 30 | "release:major": "changelog -M && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version major && npm run release:common" 31 | }, 32 | "prepublish": "npm run build", 33 | "devDependencies": { 34 | "@commitlint/cli": "^19.3.0", 35 | "@commitlint/config-conventional": "^19.2.2", 36 | "@matteo.collina/tspl": "^0.1.1", 37 | "@types/node": "^20.14.8", 38 | "@types/tap": "^15.0.11", 39 | "@typescript-eslint/eslint-plugin": "^7.13.1", 40 | "@typescript-eslint/parser": "^7.13.1", 41 | "c8": "^10.1.2", 42 | "esbuild": "^0.21.5", 43 | "eslint": "^8.29.0", 44 | "generate-changelog": "^1.8.0", 45 | "husky": "^9.0.11", 46 | "lint-staged": "^15.2.7", 47 | "prettier": "^3.3.2", 48 | "rimraf": "^5.0.7", 49 | "tsx": "^4.15.7", 50 | "typescript": "^5.5.2" 51 | }, 52 | "lint-staged": { 53 | "src/**/*.{js,jsx,ts,tsx}": [ 54 | "npx prettier --write", 55 | "npx eslint --fix" 56 | ] 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git://github.com/Cadienvan/expirables.git" 61 | }, 62 | "license": "MIT", 63 | "author": "Michael Di Prisco ", 64 | "contributors": [ 65 | { 66 | "name": "Carmelo Badalamenti", 67 | "url": "https://github.com/rollsappletree" 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /packages/website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | /docs/changelog.md 23 | /docs/contributors.md -------------------------------------------------------------------------------- /packages/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /packages/website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/website/contributors.js: -------------------------------------------------------------------------------- 1 | const http = require("https"); 2 | const fs = require('node:fs'); 3 | const path = require('node:path'); 4 | 5 | const options = { 6 | host: 'api.github.com', 7 | port: 443, 8 | path: '/repos/Cadienvan/expirables/contributors?anon=1', 9 | headers: { 'User-Agent': 'request' } 10 | }; 11 | 12 | const headerTemplate = "# Contributors\n\n| | Login | Cointributions |\n|--|--|--|\n"; 13 | const rowTemplate = '| {%LOGIN%} | [{%LOGIN%}]({%URL%}) | {%CONTRIB%} |\n'; 14 | const licenseTemplate = "\n## LICENSE\n\n {%LICENSE%}"; 15 | 16 | http.get(options, function (res) { 17 | let body = ''; 18 | let markdown = ''; 19 | 20 | if (res.statusCode != 200) { 21 | console.error(`Got response: ${res.statusCode}`); 22 | 23 | return; 24 | } 25 | 26 | res.on('data', (d) => body += d); 27 | 28 | res.on('end', function () { 29 | const contributors = JSON.parse(body); 30 | const licensePath = path.join(__dirname, '../../LICENSE'); 31 | const license = fs.readFileSync(licensePath, 'utf8'); 32 | 33 | markdown += headerTemplate; 34 | 35 | if (!license) { 36 | throw new Error('Unable to load license file'); 37 | } 38 | 39 | console.info(`Found ${contributors.length} Users`); 40 | 41 | for (const contributor of contributors) { 42 | if (contributor.type != 'User') { 43 | continue; 44 | } 45 | 46 | const compiled = rowTemplate.replaceAll('{%LOGIN%}', contributor.login) 47 | .replaceAll('{%URL%}', contributor.html_url) 48 | .replaceAll('{%AVATAR%}', contributor.avatar_url) 49 | .replaceAll('{%CONTRIB%}', contributor.contributions); 50 | 51 | markdown += compiled; 52 | } 53 | 54 | markdown += licenseTemplate.replace('{%LICENSE%}', license) 55 | .replace('2022', new Date().getFullYear()); 56 | 57 | fs.writeFileSync('./docs/contributors.md', markdown); 58 | }); 59 | }).on('error', function (e) { 60 | console.error("Error: " + e.message); 61 | }); -------------------------------------------------------------------------------- /packages/website/conventional_commit_config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | writerOpts: { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /packages/website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Getting started 6 | 7 | # What is this? 8 | 9 | This is a zero dependency package that provides some expirable implementations of common Data Structures. 10 | Thanks to the [npm-package-ts-scaffolding](https://github.com/Cadienvan/npm-package-ts-scaffolding) it is importable both via CommonJS and ES Modules. 11 | It currently supports the following Data Structures: 12 | 13 | - [ExpirableMap](map) 14 | - [ExpirableSet](set) 15 | - [ExpirableQueue](queue) 16 | - [ExpirableStack](stack) 17 | - [ExpirableLinkedList](linked-list) 18 | 19 | # How do I install it? 20 | 21 | You can install it by using the following command: 22 | 23 | ```bash 24 | npm install expirables 25 | ``` 26 | 27 | # Tests 28 | 29 | You can run the tests by using the following command: 30 | 31 | ```bash 32 | npm test 33 | ``` 34 | 35 | # Scaffolding 36 | 37 | This project was generated using Cadienvan's own [npm-package-ts-scaffolding](https://github.com/Cadienvan/npm-package-ts-scaffolding) so it has all the necessary tools to develop, test and publish a TypeScript package importable both via CommonJS and ES Modules. 38 | 39 | # FAQ 40 | 41 | ## Why are you using timeouts instead of lazy evaluation? 42 | 43 | - Lazy evaluation would need to re-implement many methods of the Data Structures and would be much more complex to implement and maintain. 44 | - Lazy evaluation could block the main thread for a long time if the Data Structure is big, and moving it to a Worker would be a lot of work for a small gain. 45 | - Lazy evaluation would require a higher amount of memory to work because it would store all the expired entries until they are evaluated. 46 | - Lazy evaluation would need us to store additional information about the entries (e.g. the expiration time) which would increase the memory footprint of the Data Structure. 47 | 48 | # Contributing 49 | 50 | If you want to contribute to this project, please open an issue or a pull request. -------------------------------------------------------------------------------- /packages/website/docs/linked-list.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Linked list 6 | 7 | Simply import the module and start using it as follows: 8 | 9 | ```js 10 | import { ExpirableLinkedList } from 'expirables'; 11 | const list = new ExpirableLinkedList(); 12 | list.append('value'); 13 | ``` 14 | 15 | # How does this work? 16 | 17 | The `ExpirableLinkedList` constructor can take two arguments: 18 | 19 | - `options` (Object, if only partially specified, the rest will be set to their default values): An object containing the following properties: 20 | - `defaultTtl` (Number): The default expiration time in milliseconds for the entries in the linked list. Defaults to `0` (never expires). 21 | - `unrefTimeouts` (Boolean): Whether or not to unref the timeout. Defaults to `false`. [Here's an explanation of what this means and why it matters you](https://nodejs.org/api/timers.html#timeoutunref). 22 | - `entries` (Array): An array of entries to initialize the linked list with. Each entry can be either a value or an array containing the value and the expiration time in milliseconds (Default: `defaultTtl`) 23 | 24 | # API 25 | 26 | ## Append 27 | 28 | You can append entries to the linked list by calling the `append` method: 29 | 30 | ```js 31 | list.append('value'); 32 | ``` 33 | 34 | You can also pass a second parameter to the `append` method to override the default expiration time (`0`) for that entry: 35 | 36 | ```js 37 | list.append('value', 2000); // 2000 is the expiration time in milliseconds for this entry 38 | ``` 39 | 40 | ## Prepend 41 | 42 | You can also prepend entries to the linked list: 43 | 44 | ```js 45 | list.prepend('value'); 46 | ``` 47 | 48 | ## Remove 49 | 50 | You can remove entries from the linked list by calling the `remove` method: 51 | 52 | ```js 53 | list.remove(element); // element is the LinkedListNode instance or the Symbol returned from the append method 54 | ``` 55 | 56 | ## setExpiration 57 | 58 | The ExpirableLinkedList also has a `setExpiration` method which takes a `Symbol` (Returned from the `push` method) or a `LinkedListNode` and a `timeInMs` arguments and expires the entry associated with that index after the specified expiration time: 59 | 60 | ```js 61 | const key = list.append('value'); 62 | list.setExpiration(key, 2000); // Expires the entry associated with the index 0 after 2000 milliseconds 63 | list.setExpiration(list.next, 2000); // This would do the same thing as the previous line 64 | ``` 65 | 66 | _**Note:** We suggest to always pass the `LinkedListNode` instance to the `setExpiration` method instead of the `Symbol` returned from the `push` method. This is because, internally, the method has to traverse the whole `LinkedList` to get the element and this can be a performance bottleneck for big lists._ 67 | 68 | ## get 69 | 70 | The ExpirableLinkedList also provides a `get` method which takes a `Symbol` (Returned from the `push` method) and returns the `LinkedListNode` associated with that key: 71 | 72 | ```js 73 | const key = list.append('value'); 74 | const node = list.get(key); 75 | ``` 76 | 77 | _**Note:** Please note that the `get` method is not a `O(1)` operation. It has to traverse the whole `LinkedList` to get the element and this can be a performance bottleneck for big lists._ 78 | 79 | # Hooks 80 | 81 | The `ExpirableLinkedList` class has hooks that you can use to execute some code in certain points of the list's lifecycle. 82 | 83 | ## Available Hooks 84 | 85 | The `ExpirableLinkedList` class has the following hooks: 86 | 87 | - `beforeExpire` (Function): A function that will be called before an entry is expired. It takes the following arguments: 88 | - `value` (Any): The value of the entry that is about to be expired. 89 | - `key` (Symbol): The key of the entry that is about to be expired. 90 | - `afterExpire` (Function): A function that will be called after an entry is expired. It takes the following arguments: 91 | - `value` (Any): The value of the entry that was expired. 92 | - `key` (Symbol): The key of the entry that was expired. 93 | 94 | ## How to use them? 95 | 96 | You can use the hooks by calling the `addHook` method on the `ExpirableLinkedList` instance: 97 | 98 | ```js 99 | const list = new ExpirableLinkedList(); 100 | list.addHook('beforeExpire', (value) => { 101 | console.log(`The value ${value} is about to expire`); 102 | }); 103 | list.addHook('afterExpire', (value) => { 104 | console.log(`The value ${value} has expired`); 105 | }); 106 | ``` 107 | 108 | ### `this` keyword 109 | 110 | The `this` keyword in the hooks will refer to the `ExpirableLinkedList` instance. 111 | 112 | # What if I append the same value multiple times? 113 | 114 | The `ExpirableLnkedList` doesn't consider `value uniqueness`. If you put the same value multiple times, it will be considered as different entries and will be expired independently. If you want to have a unique value in the linked list, please consider using the `ExpirableSet` instead. 115 | -------------------------------------------------------------------------------- /packages/website/docs/map.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Map 6 | 7 | Simply import the module and start using it as follows: 8 | 9 | ```js 10 | import { ExpirableMap } from 'expirables'; 11 | const map = new ExpirableMap(); 12 | map.set('key', 'value'); 13 | map.get('key'); // "value" 14 | ``` 15 | 16 | You can also pass a third parameter to the `set` method to override the default expiration time (`0`) for that entry: 17 | 18 | ```js 19 | map.set('key', 'value', 2000); // 2000 is the expiration time in milliseconds for this entry 20 | ``` 21 | 22 | The ExpirableMap also has a `setExpiration` method which takes a `key` and a `timeInMs` arguments and expires the entry associated with that key after the specified expiration time: 23 | 24 | ```js 25 | map.setExpiration('key', 2000); // Expires the entry associated with the key "key" after 2000 milliseconds 26 | ``` 27 | 28 | Passing `0` as the expiration time will make the entry never expire: 29 | 30 | ```js 31 | map.set('key', 'value', 0); // The entry associated with the key "key" will never expire 32 | ``` 33 | 34 | # How does this work? 35 | 36 | The `ExpirableMap` constructor can take two optional arguments: 37 | 38 | - `options` (Object, if only partially specified, the rest will be set to their default values): An object containing the following properties: 39 | - `defaultTtl` (Number): The default expiration time in milliseconds for the entries in the map. Defaults to `0` (never expires). 40 | - `keepAlive` (Boolean): Whether or not to keep alive (Re-start expiration timer) entries when set before expiring. Defaults to `true`. 41 | - `unrefTimeouts` (Boolean): Whether or not to unref the timeout. Defaults to `false`. [Here's an explanation of what this means and why it matters you](https://nodejs.org/api/timers.html#timeoutunref). 42 | - `entries` (Array): An array of entries to initialize the map with. Each entry can be either a value or an array containing the key, the value and the expiration time in milliseconds (Default: `defaultTtl`) 43 | You can simply swap a `Map` with an `ExpirableMap` and it will work as expected. 44 | 45 | # Hooks 46 | 47 | The `ExpirableMap` class has hooks that you can use to execute some code in certain points of the map's lifecycle. 48 | 49 | ## Available Hooks 50 | 51 | The `ExpirableMap` class has the following hooks: 52 | 53 | - `beforeExpire` (Function): A function that will be called before an entry is expired. It takes the following arguments: 54 | - `value` (Any): The value of the entry that is about to be expired. 55 | - `key` (Symbol): The key of the entry that is about to be expired. 56 | - `afterExpire` (Function): A function that will be called after an entry is expired. It takes the following arguments: 57 | - `value` (Any): The value of the entry that was expired. 58 | - `key` (Symbol): The key of the entry that was expired. 59 | 60 | ## How to use them? 61 | 62 | You can use the hooks by calling the `addHook` method on the `ExpirableMap` instance: 63 | 64 | ```js 65 | const map = new ExpirableMap(); 66 | map.addHook('beforeExpire', (value) => { 67 | console.log(`The value ${value} is about to expire`); 68 | }); 69 | map.addHook('afterExpire', (value) => { 70 | console.log(`The value ${value} has expired`); 71 | }); 72 | ``` 73 | 74 | ### `this` keyword 75 | 76 | The `this` keyword in the hooks will refer to the `ExpirableMap` instance. 77 | 78 | # What if I set a key that already exists? 79 | 80 | The `set` method will override the previous entry and reset the timeout for that key if the `keepAlive` option is set to `true` (default). If it is set to `false`, the timeout will not be reset. 81 | -------------------------------------------------------------------------------- /packages/website/docs/queue.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Queue 6 | 7 | Simply import the module and start using it as follows: 8 | 9 | ```js 10 | import { ExpirableQueue } from 'expirables'; 11 | const queue = new ExpirableQueue(); 12 | queue.enqueue('value'); 13 | queue.dequeue(); 14 | ``` 15 | 16 | You can also pass a second parameter to the `enqueue` method to override the default expiration time (`0`) for that entry: 17 | 18 | ```js 19 | queue.enqueue('value', 2000); // 2000 is the expiration time in milliseconds for this entry 20 | ``` 21 | 22 | The ExpirableQueue also has a `setExpiration` method which takes a `Symbol` (Returned from the `enqueue` method) and a `timeInMs` arguments and expires the entry associated with that index after the specified expiration time: 23 | 24 | ```js 25 | const key = queue.enqueue('value'); 26 | queue.setExpiration(key, 2000); // Expires the entry associated with the index 0 after 2000 milliseconds 27 | ``` 28 | 29 | # How does this work? 30 | 31 | The `ExpirableQueue` constructor can take two arguments: 32 | 33 | - `options` (Object, if only partially specified, the rest will be set to their default values): An object containing the following properties: 34 | - `defaultTtl` (Number): The default expiration time in milliseconds for the entries in the queue. Defaults to `0` (never expires). 35 | - `unrefTimeouts` (Boolean): Whether or not to unref the timeout. Defaults to `false`. [Here's an explanation of what this means and why it matters you](https://nodejs.org/api/timers.html#timeoutunref). 36 | - `entries` (Array): An array of entries to initialize the queue with. Each entry can be either a value or an array containing the value and the expiration time in milliseconds (Default: `defaultTtl`) 37 | 38 | # Hooks 39 | 40 | The `ExpirableQueue` class has hooks that you can use to execute some code in certain points of the queue's lifecycle. 41 | 42 | ## Available Hooks 43 | 44 | The `ExpirableQueue` class has the following hooks: 45 | 46 | - `beforeExpire` (Function): A function that will be called before an entry is expired. It takes the following arguments: 47 | - `value` (Any): The value of the entry that is about to be expired. 48 | - `key` (Symbol): The key of the entry that is about to be expired. 49 | - `afterExpire` (Function): A function that will be called after an entry is expired. It takes the following arguments: 50 | - `value` (Any): The value of the entry that was expired. 51 | - `key` (Symbol): The key of the entry that was expired. 52 | 53 | ## How to use them? 54 | 55 | You can use the hooks by calling the `addHook` method on the `ExpirableQueue` instance: 56 | 57 | ```js 58 | const queue = new ExpirableQueue(); 59 | queue.addHook('beforeExpire', (value) => { 60 | console.log(`The value ${value} is about to expire`); 61 | }); 62 | queue.addHook('afterExpire', (value) => { 63 | console.log(`The value ${value} has expired`); 64 | }); 65 | ``` 66 | 67 | ### `this` keyword 68 | 69 | The `this` keyword in the hooks will refer to the `ExpirableQueue` instance. 70 | 71 | # What if I put the same value multiple times? 72 | 73 | The `ExpirableQueue` doesn't consider `value uniqueness`. If you put the same value multiple times, it will be considered as different entries and will be expired independently. If you want to have a unique value in the queue, please consider using the `ExpirableSet` instead. 74 | -------------------------------------------------------------------------------- /packages/website/docs/set.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Set 6 | 7 | Simply import the module and start using it as follows: 8 | 9 | ```js 10 | import { ExpirableSet } from 'expirables'; 11 | const set = new ExpirableSet(); 12 | set.add('value'); 13 | set.has('value'); // true 14 | ``` 15 | 16 | You can also pass a second parameter to the `add` method to override the default expiration time (`0`) for that entry: 17 | 18 | ```js 19 | set.add('value', 2000); // 2000 is the expiration time in milliseconds for this entry 20 | ``` 21 | 22 | The ExpirableSet also has a `setExpiration` method which takes a `value` and a `timeInMs` arguments and expires the entry associated with that key after the specified expiration time: 23 | 24 | ```js 25 | map.setExpiration('value', 2000); // Expires the entry associated with the key "key" after 2000 milliseconds 26 | ``` 27 | 28 | Passing `0` as the expiration time will make the entry never expire: 29 | 30 | ```js 31 | map.set('key', 'value', 0); // The entry associated with the key "key" will never expire 32 | ``` 33 | 34 | N.B. As JS Sets do not have the concept of keys, the `setExpiration` method takes the same `value` passed to the `add` method as its first argument. Consider it when passing functions or objects to the `add` method. 35 | 36 | # How does this work? 37 | 38 | The `ExpirableSet` constructor can take two arguments: 39 | 40 | - `options` (Object, if only partially specified, the rest will be set to their default values): An object containing the following properties: 41 | - `defaultTtl` (Number): The default expiration time in milliseconds for the entries in the set. Defaults to `0` (never expires). 42 | - `keepAlive` (Boolean): Whether or not to keep alive (Re-start expiration timer) entries when set before expiring. Defaults to `true`. 43 | - `unrefTimeouts` (Boolean): Whether or not to unref the timeout. Defaults to `false`. [Here's an explanation of what this means and why it matters you](https://nodejs.org/api/timers.html#timeoutunref). 44 | - `entries` (Array): An array of entries to initialize the set with. Each entry can be either a value or an array containing the value and the expiration time in milliseconds (Default: `defaultTtl`) 45 | You can simply swap a `Set` with an `ExpirableSet` and it will work as expected. 46 | 47 | # Hooks 48 | 49 | The `ExpirableSet` class has hooks that you can use to execute some code in certain points of the set's lifecycle. 50 | 51 | ## Available Hooks 52 | 53 | The `ExpirableSet` class has the following hooks: 54 | 55 | - `beforeExpire` (Function): A function that will be called before an entry is expired. It takes the following arguments: 56 | - `value` (Any): The value of the entry that is about to be expired. 57 | - `afterExpire` (Function): A function that will be called after an entry is expired. It takes the following arguments: 58 | - `value` (Any): The value of the entry that was expired. 59 | 60 | ## How to use them? 61 | 62 | You can use the hooks by calling the `addHook` method on the `ExpirableSet` instance: 63 | 64 | ```js 65 | const set = new ExpirableSet(); 66 | set.addHook('beforeExpire', (value) => { 67 | console.log(`The value ${value} is about to expire`); 68 | }); 69 | set.addHook('afterExpire', (value) => { 70 | console.log(`The value ${value} has expired`); 71 | }); 72 | ``` 73 | 74 | ### `this` keyword 75 | 76 | The `this` keyword in the hooks will refer to the `ExpirableSet` instance. 77 | 78 | # What if I set a key that already exists? 79 | 80 | The `add` method will override the previous entry and reset the timeout for that key if the `keepAlive` option is set to `true` (default). If it is set to `false`, the timeout will not be reset. 81 | -------------------------------------------------------------------------------- /packages/website/docs/stack.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Stack 6 | 7 | Simply import the module and start using it as follows: 8 | 9 | ```js 10 | import { ExpirableStack } from 'expirables'; 11 | const stack = new ExpirableStack(); 12 | stack.push('value'); 13 | stack.pop(); 14 | ``` 15 | 16 | You can also pass a second parameter to the `push` method to override the default expiration time (`0`) for that entry: 17 | 18 | ```js 19 | stack.push('value', 2000); // 2000 is the expiration time in milliseconds for this entry 20 | ``` 21 | 22 | The ExpirableStack also has a `setExpiration` method which takes a `Symbol` (Returned from the `push` method) and a `timeInMs` arguments and expires the entry associated with that index after the specified expiration time: 23 | 24 | ```js 25 | const key = stack.push('value'); 26 | stack.setExpiration(key, 2000); // Expires the entry associated with the index 0 after 2000 milliseconds 27 | ``` 28 | 29 | # How does this work? 30 | 31 | The `ExpirableStack` constructor can take two arguments: 32 | 33 | - `options` (Object, if only partially specified, the rest will be set to their default values): An object containing the following properties: 34 | - `defaultTtl` (Number): The default expiration time in milliseconds for the entries in the stack. Defaults to `0` (never expires). 35 | - `unrefTimeouts` (Boolean): Whether or not to unref the timeout. Defaults to `false`. [Here's an explanation of what this means and why it matters you](https://nodejs.org/api/timers.html#timeoutunref). 36 | - `entries` (Array): An array of entries to initialize the stack with. Each entry can be either a value or an array containing the value and the expiration time in milliseconds (Default: `defaultTtl`) 37 | 38 | # Hooks 39 | 40 | The `ExpirableStack` class has hooks that you can use to execute some code in certain points of the stack's lifecycle. 41 | 42 | ## Available Hooks 43 | 44 | The `ExpirableStack` class has the following hooks: 45 | 46 | - `beforeExpire` (Function): A function that will be called before an entry is expired. It takes the following arguments: 47 | - `value` (Any): The value of the entry that is about to be expired. 48 | - `key` (Symbol): The key of the entry that is about to be expired. 49 | - `afterExpire` (Function): A function that will be called after an entry is expired. It takes the following arguments: 50 | - `value` (Any): The value of the entry that was expired. 51 | - `key` (Symbol): The key of the entry that was expired. 52 | 53 | ## How to use them? 54 | 55 | You can use the hooks by calling the `addHook` method on the `ExpirableStack` instance: 56 | 57 | ```js 58 | const queue = new ExpirableStack(); 59 | queue.addHook('beforeExpire', (value) => { 60 | console.log(`The value ${value} is about to expire`); 61 | }); 62 | queue.addHook('afterExpire', (value) => { 63 | console.log(`The value ${value} has expired`); 64 | }); 65 | ``` 66 | 67 | ### `this` keyword 68 | 69 | The `this` keyword in the hooks will refer to the `ExpirableStack` instance. 70 | 71 | # What if I put the same value multiple times? 72 | 73 | The `ExpirableStack` doesn't consider `value uniqueness`. If you put the same value multiple times, it will be considered as different entries and will be expired independently. If you want to have a unique value in the stack, please consider using the `ExpirableSet` instead. 74 | -------------------------------------------------------------------------------- /packages/website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'Expirables', 10 | tagline: 'This is a zero dependency package that provides some expirable implementations of common Data Structures.', 11 | favicon: 'img/favicon.ico', 12 | 13 | // Set the production url of your site here 14 | url: 'https://github.com', 15 | // Set the // pathname under which your site is served 16 | // For GitHub pages deployment, it is often '//' 17 | baseUrl: '/', 18 | 19 | // GitHub pages deployment config. 20 | // If you aren't using GitHub pages, you don't need these. 21 | organizationName: 'Cadienvan', // Usually your GitHub org/user name. 22 | projectName: 'expirables', // Usually your repo name. 23 | 24 | onBrokenLinks: 'throw', 25 | onBrokenMarkdownLinks: 'warn', 26 | 27 | // Even if you don't use internalization, you can use this field to set useful 28 | // metadata like html lang. For example, if your site is Chinese, you may want 29 | // to replace "en" with "zh-Hans". 30 | i18n: { 31 | defaultLocale: 'en', 32 | locales: ['en'], 33 | }, 34 | 35 | presets: [ 36 | [ 37 | 'classic', 38 | /** @type {import('@docusaurus/preset-classic').Options} */ 39 | ({ 40 | docs: { 41 | sidebarPath: require.resolve('./sidebars.js'), 42 | // Please change this to your repo. 43 | // Remove this to remove the "edit this page" links. 44 | }, 45 | theme: { 46 | customCss: require.resolve('./src/css/custom.css'), 47 | }, 48 | }), 49 | ], 50 | ], 51 | 52 | themeConfig: 53 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 54 | ({ 55 | // Replace with your project's social card 56 | image: 'img/expirables.png', 57 | navbar: { 58 | title: 'Expirables', 59 | logo: { 60 | alt: 'Expirables logo', 61 | src: 'img/expirables-logo.png', 62 | }, 63 | items: [ 64 | { 65 | href: 'https://github.com/Cadienvan/expirables/', 66 | label: 'GitHub', 67 | position: 'right', 68 | }, 69 | ], 70 | }, 71 | footer: { 72 | style: 'dark', 73 | links: [ 74 | { 75 | title: 'Docs', 76 | items: [ 77 | { 78 | label: 'Documentation', 79 | to: '/docs/intro', 80 | }, 81 | { 82 | label: 'Change log', 83 | to: '/docs/changelog', 84 | }, 85 | ], 86 | }, 87 | { 88 | title: 'Community', 89 | items: [ 90 | { 91 | label: 'Issues', 92 | href: 'https://github.com/Cadienvan/expirables/issues', 93 | }, 94 | { 95 | label: 'Project', 96 | href: 'https://github.com/Cadienvan/expirables/projects', 97 | }, 98 | ], 99 | }, 100 | { 101 | title: 'More', 102 | items: [ 103 | { 104 | label: 'GitHub', 105 | href: 'https://github.com/Cadienvan/expirables/', 106 | }, 107 | ], 108 | }, 109 | ], 110 | copyright: `Copyright © ${new Date().getFullYear()} cadienvan.`, 111 | }, 112 | prism: { 113 | theme: lightCodeTheme, 114 | darkTheme: darkCodeTheme, 115 | }, 116 | }), 117 | }; 118 | 119 | module.exports = config; 120 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expirables/docs", 3 | "version": "0.0.1", 4 | "private": false, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc", 16 | "changelog": "conventional-changelog -i docs/changelog.md -s -r 0 -c conventional_commit_config.js && echo \"# Change log\n\n$(cat docs/changelog.md)\" > docs/changelog.md", 17 | "contributors": "node contributors.js", 18 | "prebuild": "npm run changelog && npm run contributors" 19 | }, 20 | "dependencies": { 21 | "@docusaurus/core": "^3.1.0", 22 | "@docusaurus/preset-classic": "^3.0.1", 23 | "@mdx-js/react": "^3.0.0", 24 | "clsx": "^1.2.1", 25 | "prism-react-renderer": "^1.3.5", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "3.0.1", 31 | "@tsconfig/docusaurus": "^1.0.5", 32 | "conventional-changelog-cli": "^3.0.0", 33 | "typescript": "^4.7.4" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=16.14" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /packages/website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.module.css'; 3 | import useBaseUrl from '@docusaurus/useBaseUrl'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: string; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Common structures', 14 | Svg: '/img/undraw_task_re_wi3v.svg', 15 | description: ( 16 | <> 17 | Map, Set, Queue, Stack, and Linked List. 18 | 19 | ), 20 | }, 21 | { 22 | title: 'Zero dependencies', 23 | Svg: '/img/undraw_delivery_truck_vt6p.svg', 24 | description: ( 25 | <> 26 | No need of third party packages to run. 27 | 28 | ), 29 | }, 30 | { 31 | title: 'Free', 32 | Svg: '/img/undraw_open_source_-1-qxw.svg', 33 | description: ( 34 | <> 35 | Based on MIT license. 36 | 37 | ), 38 | }, 39 | ]; 40 | 41 | function Feature({title, Svg, description}: FeatureItem) { 42 | return ( 43 |
44 |
45 | 46 |
47 |
48 |

{title}

49 |

{description}

50 |
51 |
52 | ); 53 | } 54 | 55 | export default function HomepageFeatures(): JSX.Element { 56 | return ( 57 |
58 |
59 |
60 | 61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /packages/website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #42cdd1; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #42cdd1; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | 32 | .center { 33 | text-align: center; 34 | margin: 0 auto; 35 | } -------------------------------------------------------------------------------- /packages/website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /packages/website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | import styles from './index.module.css'; 8 | 9 | function HomepageHeader() { 10 | const {siteConfig} = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 |

{siteConfig.title}

15 |

{siteConfig.tagline}

16 |
17 | 20 | Getting Started ⏱️ 21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | export default function Home(): JSX.Element { 29 | const {siteConfig} = useDocusaurusContext(); 30 | return ( 31 | 34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/.nojekyll -------------------------------------------------------------------------------- /packages/website/static/img/docusaurus-social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/img/docusaurus-social-card.png -------------------------------------------------------------------------------- /packages/website/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/img/docusaurus.png -------------------------------------------------------------------------------- /packages/website/static/img/expirables-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/img/expirables-logo.png -------------------------------------------------------------------------------- /packages/website/static/img/expirables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/img/expirables.png -------------------------------------------------------------------------------- /packages/website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadienvan/expirables/700304e6a545343906f42770b593a76cd2d39328/packages/website/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/website/static/img/undraw_delivery_truck_vt6p.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/static/img/undraw_open_source_-1-qxw.svg: -------------------------------------------------------------------------------- 1 | open source -------------------------------------------------------------------------------- /packages/website/static/img/undraw_task_re_wi3v.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/LinkedList/Node.ts: -------------------------------------------------------------------------------- 1 | export default class LinkedListNode { 2 | id: Symbol = Symbol(); 3 | value: Val; 4 | next: LinkedListNode | null; 5 | 6 | constructor(value: Val, next = null) { 7 | this.id = Symbol(); 8 | this.value = value; 9 | this.next = next; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/LinkedList/index.ts: -------------------------------------------------------------------------------- 1 | // Create an expirable stack based on the ExpirableMap class 2 | 3 | import { NOT_EXPIRING_TTL, TTL } from '../utils'; 4 | import LinkedListNode from './Node'; 5 | import type { ExpirableLinkedListOptions } from '../types'; 6 | import { addHook, runHook } from '../utils/hooks'; 7 | 8 | const defaultOptions: ExpirableLinkedListOptions = { 9 | defaultTtl: NOT_EXPIRING_TTL, 10 | unrefTimeouts: false 11 | }; 12 | 13 | enum Hooks { 14 | beforeExpire = 'beforeExpire', 15 | afterExpire = 'afterExpire' 16 | } 17 | 18 | export class ExpirableLinkedList { 19 | public readonly [Symbol.toStringTag] = 'ExpirableLinkedList'; 20 | timeouts: Map; 21 | options: ExpirableLinkedListOptions; 22 | head: LinkedListNode | null; 23 | tail: LinkedListNode | null; 24 | hooks = new Set(Object.values(Hooks)); 25 | 26 | addHook = addHook; 27 | runHook = runHook; 28 | 29 | constructor( 30 | entries: Array | Array<[Val, TTL]> = [], 31 | options: Partial = {} 32 | ) { 33 | this.timeouts = new Map(); 34 | this.options = { ...defaultOptions, ...options }; 35 | this.head = null; 36 | this.tail = null; 37 | if (entries) { 38 | for (const entry of entries) { 39 | if (entry instanceof Array) { 40 | this.append(entry[0], entry[1] || this.options.defaultTtl); 41 | } else { 42 | this.append(entry, this.options.defaultTtl); 43 | } 44 | } 45 | } 46 | } 47 | 48 | append(value: Val, ttl: TTL = this.options.defaultTtl) { 49 | const node = new LinkedListNode(value); 50 | if (this.head === null) { 51 | this.head = node; 52 | this.tail = node; 53 | } else { 54 | this.tail!.next = node; 55 | this.tail = node; 56 | } 57 | 58 | if (ttl !== NOT_EXPIRING_TTL) { 59 | this.setExpiration(node, ttl); 60 | } 61 | 62 | return node.id; 63 | } 64 | 65 | prepend(value: Val, ttl: TTL = this.options.defaultTtl) { 66 | const node = new LinkedListNode(value); 67 | if (this.head === null) { 68 | this.head = node; 69 | this.tail = node; 70 | } else { 71 | node.next = this.head; 72 | this.head = node; 73 | } 74 | 75 | if (ttl !== NOT_EXPIRING_TTL) { 76 | this.setExpiration(node, ttl); 77 | } 78 | 79 | return node.id; 80 | } 81 | 82 | remove(param: Symbol | LinkedListNode) { 83 | const id = param instanceof LinkedListNode ? param.id : param; 84 | let node = this.head; 85 | let prev: LinkedListNode | null = null; 86 | 87 | while (node !== null) { 88 | if (node.id === id) { 89 | if (prev !== null) { 90 | prev.next = node.next; 91 | } else { 92 | this.head = node.next; 93 | } 94 | 95 | if (node.next === null) { 96 | this.tail = prev; 97 | } 98 | 99 | const timeout = this.timeouts.get(node.id); 100 | if (timeout) { 101 | clearTimeout(timeout); 102 | this.timeouts.delete(node.id); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | prev = node; 109 | node = node.next; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | clear() { 116 | this.head = null; 117 | this.tail = null; 118 | this.timeouts.forEach((timeout) => clearTimeout(timeout)); 119 | this.timeouts.clear(); 120 | } 121 | 122 | setExpiration(param: Symbol | LinkedListNode, ttl: TTL) { 123 | if (ttl === NOT_EXPIRING_TTL) return this; 124 | 125 | const id = param instanceof LinkedListNode ? param.id : param; 126 | const el = param instanceof LinkedListNode ? param : this.get(id); 127 | if (!el) return this; 128 | 129 | const timeout = this.timeouts.get(id); 130 | 131 | /* c8 ignore next 4 */ 132 | if (timeout) { 133 | clearTimeout(timeout); 134 | this.timeouts.delete(id); 135 | } 136 | 137 | if (ttl !== NOT_EXPIRING_TTL) { 138 | const timeout = setTimeout(() => { 139 | this.runHook(Hooks.beforeExpire, el.value, id); 140 | this.remove(id); 141 | this.runHook(Hooks.afterExpire, el.value, id); 142 | }, ttl); 143 | 144 | if (this.options.unrefTimeouts) { 145 | timeout.unref(); 146 | } 147 | 148 | this.timeouts.set(id, timeout); 149 | } 150 | 151 | return this; 152 | } 153 | 154 | get(id: Symbol): LinkedListNode | undefined { 155 | let node = this.head; 156 | 157 | while (node !== null) { 158 | if (node.id === id) { 159 | return node; 160 | } 161 | 162 | node = node.next; 163 | } 164 | 165 | return undefined; 166 | } 167 | 168 | get length() { 169 | let node = this.head; 170 | let length = 0; 171 | 172 | while (node !== null) { 173 | length++; 174 | node = node.next; 175 | } 176 | 177 | return length; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Map/index.ts: -------------------------------------------------------------------------------- 1 | import { NOT_EXPIRING_TTL, TTL } from '../utils'; 2 | import type { ExpirableMapOptions } from '../types'; 3 | import { addHook, runHook } from '../utils/hooks'; 4 | 5 | const defaultOptions: ExpirableMapOptions = { 6 | defaultTtl: NOT_EXPIRING_TTL, 7 | keepAlive: true, 8 | unrefTimeouts: false 9 | }; 10 | 11 | enum Hooks { 12 | beforeExpire = 'beforeExpire', 13 | afterExpire = 'afterExpire' 14 | } 15 | 16 | export class ExpirableMap extends Map { 17 | public readonly [Symbol.toStringTag] = 'ExpirableMap'; 18 | timeouts: Map; 19 | options: ExpirableMapOptions; 20 | hooks = new Set(Object.values(Hooks)); 21 | 22 | addHook = addHook; 23 | runHook = runHook; 24 | 25 | constructor( 26 | entries: Array<[Key, Val, TTL?]> = [], 27 | options: Partial = defaultOptions 28 | ) { 29 | super(); 30 | this.options = { ...defaultOptions, ...options }; 31 | this.timeouts = new Map(); 32 | if (entries) 33 | entries.forEach((entry) => 34 | this.set(entry[0], entry[1], entry[2] || this.options.defaultTtl) 35 | ); 36 | } 37 | 38 | setExpiration(key: Key, timeInMs = this.options.defaultTtl) { 39 | if (timeInMs === NOT_EXPIRING_TTL) return this; 40 | 41 | if (this.timeouts.has(key)) this.clearTimeout(key); 42 | 43 | if (!this.has(key)) return; 44 | const value = this.get(key); 45 | 46 | const timeout = setTimeout(() => { 47 | this.runHook(Hooks.beforeExpire, value, key); 48 | this.delete(key); 49 | this.runHook(Hooks.afterExpire, value, key); 50 | }, timeInMs); 51 | this.timeouts.set( 52 | key, 53 | this.options.unrefTimeouts ? timeout.unref() : timeout 54 | ); 55 | 56 | return this; 57 | } 58 | 59 | set(key: Key, value: Val, ttl = this.options.defaultTtl) { 60 | if (!Number.isFinite(ttl)) throw new Error('TTL must be a number'); 61 | if (this.options.keepAlive) this.clearTimeout(key); 62 | 63 | const hasKey = this.has(key); 64 | const result = super.set(key, value); 65 | 66 | if ((this.options.keepAlive || !hasKey) && ttl !== NOT_EXPIRING_TTL) 67 | this.setExpiration(key, ttl); 68 | 69 | return result; 70 | } 71 | 72 | delete(key: Key) { 73 | this.clearTimeout(key); 74 | return super.delete(key); 75 | } 76 | 77 | clearTimeout(key: Key) { 78 | clearTimeout(this.timeouts.get(key)); 79 | this.timeouts.delete(key); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Queue/index.ts: -------------------------------------------------------------------------------- 1 | // Create an expirable queue based on the ExpirableMap class 2 | 3 | import { NOT_EXPIRING_TTL, TTL } from '../utils'; 4 | import type { ExpirableQueueOptions } from '../types'; 5 | import { addHook, runHook } from '../utils/hooks'; 6 | 7 | const defaultOptions: ExpirableQueueOptions = { 8 | defaultTtl: NOT_EXPIRING_TTL, 9 | unrefTimeouts: false 10 | }; 11 | 12 | enum Hooks { 13 | beforeExpire = 'beforeExpire', 14 | afterExpire = 'afterExpire' 15 | } 16 | 17 | export class ExpirableQueue { 18 | public readonly [Symbol.toStringTag] = 'ExpirableQueue'; 19 | timeouts: Map; 20 | options: ExpirableQueueOptions; 21 | elements: Array<{ key: Symbol; value: Val }> = []; 22 | hooks = new Set(Object.values(Hooks)); 23 | 24 | addHook = addHook; 25 | runHook = runHook; 26 | 27 | constructor( 28 | entries: Array | Array<[Val, TTL]> = [], 29 | options: Partial = defaultOptions 30 | ) { 31 | this.options = { ...defaultOptions, ...options }; 32 | this.timeouts = new Map(); 33 | if (entries) { 34 | for (const entry of entries) { 35 | if (entry instanceof Array) { 36 | this.enqueue(entry[0], entry[1] || this.options.defaultTtl); 37 | } else { 38 | this.enqueue(entry, this.options.defaultTtl); 39 | } 40 | } 41 | } 42 | } 43 | 44 | enqueue(value: Val, ttl = this.options.defaultTtl): Symbol { 45 | if (!Number.isFinite(ttl)) throw new Error('TTL must be a number'); 46 | const key = Symbol(); 47 | this.elements.push({ key, value }); 48 | if (ttl !== NOT_EXPIRING_TTL) this.setExpiration(key, ttl); 49 | return key; 50 | } 51 | 52 | dequeue() { 53 | if (this.elements.length === 0) return; 54 | const element = this.elements.shift(); 55 | /* c8 ignore next */ 56 | if (typeof element === 'undefined') return; 57 | const { key, value } = element; 58 | this.clearTimeout(key); 59 | return value; 60 | } 61 | 62 | delete(key: Symbol) { 63 | this.elements = this.elements.filter((element) => element.key !== key); 64 | this.clearTimeout(key); 65 | } 66 | 67 | setExpiration(key: Symbol, timeInMs = this.options.defaultTtl) { 68 | if (timeInMs === NOT_EXPIRING_TTL) return this; 69 | 70 | /* c8 ignore next */ 71 | if (this.timeouts.has(key)) this.clearTimeout(key); 72 | const el = this.elements.find((e) => e.key === key); 73 | if (!el) return this; 74 | 75 | const timeout = setTimeout(() => { 76 | this.runHook(Hooks.beforeExpire, el.value, key); 77 | this.delete(key); 78 | this.runHook(Hooks.afterExpire, el.value, key); 79 | }, timeInMs); 80 | this.timeouts.set( 81 | key, 82 | this.options.unrefTimeouts ? timeout.unref() : timeout 83 | ); 84 | return this; 85 | } 86 | 87 | clearTimeout(key: Symbol) { 88 | clearTimeout(this.timeouts.get(key)); 89 | this.timeouts.delete(key); 90 | } 91 | 92 | get size() { 93 | return this.elements.length; 94 | } 95 | 96 | get next() { 97 | return this.elements[0]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Set/index.ts: -------------------------------------------------------------------------------- 1 | import { NOT_EXPIRING_TTL, TTL } from '../utils'; 2 | import type { ExpirableSetOptions } from '../types'; 3 | import { addHook, runHook } from '../utils/hooks'; 4 | 5 | const defaultOptions: ExpirableSetOptions = { 6 | defaultTtl: 0, 7 | keepAlive: true, 8 | unrefTimeouts: false 9 | }; 10 | 11 | enum Hooks { 12 | beforeExpire = 'beforeExpire', 13 | afterExpire = 'afterExpire' 14 | } 15 | 16 | export class ExpirableSet extends Set { 17 | public readonly [Symbol.toStringTag] = 'ExpirableSet'; 18 | timeouts: Map; 19 | options: ExpirableSetOptions; 20 | hooks = new Set(Object.values(Hooks)); 21 | 22 | addHook = addHook; 23 | runHook = runHook; 24 | 25 | constructor( 26 | entries: Array | Array<[Val, TTL]> = [], 27 | options: Partial = defaultOptions 28 | ) { 29 | super(); 30 | this.options = { ...defaultOptions, ...options }; 31 | this.timeouts = new Map(); 32 | if (entries) { 33 | for (const entry of entries) { 34 | if (entry instanceof Array) { 35 | this.add(entry[0], entry[1] || this.options.defaultTtl); 36 | } else { 37 | this.add(entry, this.options.defaultTtl); 38 | } 39 | } 40 | } 41 | } 42 | 43 | setExpiration(value: Val, timeInMs = this.options.defaultTtl) { 44 | if (timeInMs === NOT_EXPIRING_TTL) return this; 45 | 46 | /* c8 ignore next 2 */ 47 | if (this.timeouts.has(value)) this.clearTimeout(value); 48 | if (!this.has(value)) return this; 49 | 50 | const timeout = setTimeout(() => { 51 | this.runHook(Hooks.beforeExpire, value); 52 | this.delete(value); 53 | this.runHook(Hooks.afterExpire, value); 54 | }, timeInMs); 55 | this.timeouts.set( 56 | value, 57 | this.options.unrefTimeouts ? timeout.unref() : timeout 58 | ); 59 | 60 | return this; 61 | } 62 | 63 | add(value: Val, ttl = this.options.defaultTtl) { 64 | if (!Number.isFinite(ttl)) throw new Error('TTL must be a number'); 65 | if (this.options.keepAlive) this.clearTimeout(value); 66 | 67 | const hasKey = this.has(value); 68 | const result = super.add(value); 69 | 70 | if ((this.options.keepAlive || !hasKey) && ttl !== NOT_EXPIRING_TTL) 71 | this.setExpiration(value, ttl); 72 | return result; 73 | } 74 | 75 | delete(value: Val) { 76 | this.clearTimeout(value); 77 | return super.delete(value); 78 | } 79 | 80 | clearTimeout(value: Val) { 81 | clearTimeout(this.timeouts.get(value)); 82 | this.timeouts.delete(value); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Stack/index.ts: -------------------------------------------------------------------------------- 1 | // Create an expirable stack based on the ExpirableMap class 2 | 3 | import { NOT_EXPIRING_TTL, TTL } from '../utils'; 4 | import type { ExpirableStackOptions } from '../types'; 5 | import { addHook, runHook } from '../utils/hooks'; 6 | 7 | const defaultOptions: ExpirableStackOptions = { 8 | defaultTtl: NOT_EXPIRING_TTL, 9 | unrefTimeouts: false 10 | }; 11 | 12 | enum Hooks { 13 | beforeExpire = 'beforeExpire', 14 | afterExpire = 'afterExpire' 15 | } 16 | 17 | export class ExpirableStack { 18 | public readonly [Symbol.toStringTag] = 'ExpirableStack'; 19 | timeouts: Map; 20 | options: ExpirableStackOptions; 21 | elements: Array<{ key: Symbol; value: Val }> = []; 22 | hooks = new Set(Object.values(Hooks)); 23 | 24 | addHook = addHook; 25 | runHook = runHook; 26 | 27 | constructor( 28 | entries: Array | Array<[Val, TTL]> = [], 29 | options: Partial = defaultOptions 30 | ) { 31 | this.options = { ...defaultOptions, ...options }; 32 | this.timeouts = new Map(); 33 | if (entries) { 34 | for (const entry of entries) { 35 | if (entry instanceof Array) { 36 | this.push(entry[0], entry[1] || this.options.defaultTtl); 37 | } else { 38 | this.push(entry, this.options.defaultTtl); 39 | } 40 | } 41 | } 42 | } 43 | push(value: Val, ttl = this.options.defaultTtl): Symbol { 44 | if (!Number.isFinite(ttl)) throw new Error('TTL must be a number'); 45 | const key = Symbol(); 46 | this.elements.push({ key, value }); 47 | if (ttl !== NOT_EXPIRING_TTL) this.setExpiration(key, ttl); 48 | return key; 49 | } 50 | 51 | pop() { 52 | const element = this.elements.shift(); 53 | if (typeof element === 'undefined') return; 54 | const { key, value } = element; 55 | this.clearTimeout(key); 56 | return value; 57 | } 58 | 59 | delete(key: Symbol) { 60 | this.elements = this.elements.filter((element) => element.key !== key); 61 | this.clearTimeout(key); 62 | } 63 | 64 | setExpiration(key: Symbol, timeInMs = this.options.defaultTtl) { 65 | if (timeInMs === NOT_EXPIRING_TTL) return this; 66 | 67 | /* c8 ignore next */ 68 | if (this.timeouts.has(key)) this.clearTimeout(key); 69 | 70 | const el = this.elements.find((e) => e.key === key); 71 | if (!el) return this; 72 | const timeout = setTimeout(() => { 73 | this.runHook(Hooks.beforeExpire, el.value, key); 74 | this.delete(key); 75 | this.runHook(Hooks.afterExpire, el.value, key); 76 | }, timeInMs); 77 | this.timeouts.set( 78 | key, 79 | this.options.unrefTimeouts ? timeout.unref() : timeout 80 | ); 81 | return this; 82 | } 83 | 84 | clearTimeout(key: Symbol) { 85 | clearTimeout(this.timeouts.get(key)); 86 | this.timeouts.delete(key); 87 | } 88 | 89 | get size() { 90 | return this.elements.length; 91 | } 92 | 93 | get next() { 94 | return this.elements.length > 0 95 | ? this.elements[this.elements.length - 1].value 96 | : undefined; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LinkedList'; 2 | export * from './Map'; 3 | export * from './Set'; 4 | export * from './Queue'; 5 | export * from './Stack'; 6 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type SharedOptions = { 2 | defaultTtl: number; 3 | unrefTimeouts: boolean; 4 | }; 5 | 6 | export type ExpirableLinkedListOptions = SharedOptions; 7 | 8 | export type ExpirableMapOptions = SharedOptions & { 9 | keepAlive?: boolean; 10 | }; 11 | 12 | export type ExpirableQueueOptions = SharedOptions; 13 | 14 | export type ExpirableSetOptions = SharedOptions & { 15 | keepAlive?: boolean; 16 | }; 17 | 18 | export type ExpirableStackOptions = SharedOptions; 19 | -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | export function addHook(this: any, name: string, fn: (...args: any[]) => void) { 2 | if (this.hooks && !this.hooks.has(name)) 3 | throw new Error(`Hook ${name} does not exist in ${this.toString()}`); 4 | this[keys.kHooks] = 5 | this[keys.kHooks] || (new Map() as Map>); 6 | const hooks = this[keys.kHooks].get(name) || new Set(); 7 | hooks.add(fn); 8 | this[keys.kHooks].set(name, hooks); 9 | } 10 | 11 | export function runHook(this: any, name: string, ...args: any[]) { 12 | if (this.hooks && !this.hooks.has(name)) 13 | throw new Error(`Hook ${name} does not exist in ${this.toString()}`); 14 | if (!this[keys.kHooks] || !this[keys.kHooks].get(name)) return; 15 | for (const hook of this[keys.kHooks].get(name)) { 16 | hook.call(this, ...args); 17 | } 18 | } 19 | 20 | export const keys = { 21 | kHooks: Symbol('hooks') 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ttl'; 2 | export * from './timers'; 3 | -------------------------------------------------------------------------------- /src/utils/timers.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | 3 | export const sleep = promisify(setTimeout); 4 | -------------------------------------------------------------------------------- /src/utils/ttl.ts: -------------------------------------------------------------------------------- 1 | export type TTL = number; 2 | export const NOT_EXPIRING_TTL = 0; 3 | -------------------------------------------------------------------------------- /test/linked-list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { tspl } from '@matteo.collina/tspl'; 3 | import { ExpirableLinkedList } from '../src/LinkedList/index.js'; 4 | import { sleep } from '../src/utils/index.js'; 5 | 6 | describe('LinkedList', async () => { 7 | it('should create a linked list', (t) => { 8 | const { equal } = tspl(t, { plan: 4 }); 9 | 10 | const emptyList = new ExpirableLinkedList(); 11 | const simpleList = new ExpirableLinkedList([1, 2, 3]); 12 | const simpleListWithDefaultTtl = new ExpirableLinkedList([1, 2, 3], { 13 | defaultTtl: 10 14 | }); 15 | const complexList = new ExpirableLinkedList([[1], 2, [3, 10]]); 16 | 17 | equal(emptyList.length, 0); 18 | equal(simpleList.length, 3); 19 | equal(simpleListWithDefaultTtl.length, 3); 20 | equal(complexList.length, 3); 21 | }); 22 | 23 | it('should act as a LinkedList if defaultTtl is 0', (t) => { 24 | const { equal } = tspl(t, { plan: 3 }); 25 | 26 | const list = new ExpirableLinkedList([1, 2, 3]); 27 | 28 | equal(list.head?.value, 1); 29 | equal(list.head?.next?.value, 2); 30 | equal(list.tail?.value, 3); 31 | }); 32 | 33 | it('should prepend an element in both an empty and filled list', (t) => { 34 | const { equal } = tspl(t, { plan: 6 }); 35 | 36 | const list = new ExpirableLinkedList([1, 2, 3, 4]); 37 | 38 | list.prepend(4, 1000); // 4 will expire after 1s, adding second parameter only for coverage 39 | 40 | equal(list.head?.value, 4); 41 | equal(list.head?.next?.value, 1); 42 | equal(list.head?.next?.next?.value, 2); 43 | equal(list.head?.next?.next?.next?.value, 3); 44 | equal(list.tail?.value, 4); 45 | 46 | const list2 = new ExpirableLinkedList(); 47 | list2.prepend(4); 48 | 49 | equal(list2.head?.value, 4); 50 | }); 51 | 52 | it('should clear the list', (t) => { 53 | const { equal } = tspl(t, { plan: 2 }); 54 | 55 | const list = new ExpirableLinkedList([1, 2, 3]); 56 | equal(list.length, 3); 57 | list.clear(); 58 | equal(list.length, 0); 59 | }); 60 | 61 | it('should return false when removing an element that does not exist', (t) => { 62 | const { equal } = tspl(t, { plan: 1 }); 63 | 64 | const list = new ExpirableLinkedList([1, 2, 3]); 65 | equal(list.remove(Symbol()), false); 66 | }); 67 | 68 | it('should remove the first element after the expiration time', async (t) => { 69 | const { equal } = tspl(t, { plan: 1 }); 70 | 71 | const list = new ExpirableLinkedList([1, 2, 3], { 72 | defaultTtl: 10, 73 | unrefTimeouts: true 74 | }); 75 | 76 | await sleep(20); 77 | 78 | equal(list.head, null); 79 | }); 80 | 81 | it('should set an expirable entry and remote it after the expiration time', async (t) => { 82 | const { equal } = tspl(t, { plan: 4 }); 83 | 84 | const list = new ExpirableLinkedList([1, 2, 3], { 85 | defaultTtl: 10 86 | }); 87 | 88 | equal(list.length, 3); 89 | await sleep(20); 90 | equal(list.length, 0); 91 | 92 | list.prepend(4, 30); 93 | await sleep(10); 94 | equal(list.length, 1); 95 | 96 | await sleep(20); 97 | equal(list.length, 0); 98 | }); 99 | 100 | it('should allow entries to define a specific ttl and let them expire accordingly', async (t) => { 101 | const { equal } = tspl(t, { plan: 4 }); 102 | 103 | const list = new ExpirableLinkedList([[1, 30], 2, [3, 50]], { 104 | defaultTtl: 10 105 | }); // 2 will expire after 10ms, 1 after 30ms and 3 after 50ms 106 | 107 | equal(list.length, 3); 108 | await sleep(11); 109 | equal(list.length, 2); 110 | await sleep(20); 111 | equal(list.length, 1); 112 | await sleep(20); 113 | equal(list.length, 0); 114 | }); 115 | 116 | it('should remove an entry when remove is called by using the id', (t) => { 117 | const { equal } = tspl(t, { plan: 4 }); 118 | 119 | const list = new ExpirableLinkedList([1, 2, 3], { defaultTtl: 10 }); 120 | equal(list.length, 3); 121 | list.remove(list.head!.id); 122 | equal(list.length, 2); 123 | list.remove(list.head!.id); 124 | equal(list.length, 1); 125 | list.remove(list.head!.id); 126 | equal(list.length, 0); 127 | }); 128 | 129 | it('should remove an entry when remove is called by using the node', (t) => { 130 | const { equal } = tspl(t, { plan: 4 }); 131 | 132 | const list = new ExpirableLinkedList([1, 2, 3], { defaultTtl: 10 }); 133 | equal(list.length, 3); 134 | list.remove(list.head!); 135 | equal(list.length, 2); 136 | list.remove(list.head!); 137 | equal(list.length, 1); 138 | list.remove(list.head!); 139 | equal(list.length, 0); 140 | }); 141 | 142 | it('should get the correct entry when get is called by using the id', (t) => { 143 | const { equal } = tspl(t, { plan: 2 }); 144 | 145 | const list = new ExpirableLinkedList(); 146 | const key1 = list.append(1); 147 | const key2 = list.append(2); 148 | 149 | equal(list.get(key1)?.value, 1); 150 | equal(list.get(key2)?.value, 2); 151 | }); 152 | 153 | it('should return undefined if the node is null', (t) => { 154 | const { equal } = tspl(t, { plan: 1 }); 155 | 156 | const list = new ExpirableLinkedList(); 157 | const key = list.append(1); 158 | 159 | // @ts-expect-error - we're testing this 160 | list.head! = null; 161 | 162 | equal(list.get(key), undefined); 163 | }); 164 | 165 | it('should not expire with ttl 0 in setExpiration', async (t) => { 166 | const { equal } = tspl(t, { plan: 3 }); 167 | 168 | const list = new ExpirableLinkedList(); 169 | const val = Symbol(1); 170 | 171 | list.append(val); 172 | equal(list.length, 1); 173 | equal(list.head?.value, val); 174 | 175 | list.setExpiration(val, 0); 176 | 177 | await sleep(20); 178 | 179 | equal(list.length, 1); 180 | }); 181 | 182 | it('should instance the id as linked node and as value', async (t) => { 183 | const { equal } = tspl(t, { plan: 4 }); 184 | 185 | const list = new ExpirableLinkedList(); 186 | const list2 = new ExpirableLinkedList([1], { defaultTtl: 10 }); 187 | 188 | list.append(list2); 189 | 190 | equal(list.length, 1); 191 | equal(list.head?.value, list2); 192 | 193 | // @ts-expect-error - we're testing this 194 | list.setExpiration(list2, 10); 195 | 196 | await sleep(20); 197 | 198 | equal(list.length, 1); 199 | equal(list2.length, 0); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/map.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { tspl } from '@matteo.collina/tspl'; 3 | import { ExpirableMap } from '../src/Map/index.js'; 4 | import { sleep } from '../src/utils/index.js'; 5 | 6 | describe('ExpirableMap', async () => { 7 | it('should create a map', (t) => { 8 | const { equal } = tspl(t, { plan: 3 }); 9 | 10 | const emptyMap = new ExpirableMap(); 11 | const simpleMap = new ExpirableMap([ 12 | ['a', 1, 10], 13 | ['b', 2, 20] 14 | ]); 15 | const simpleMapWithDefaultTtl = new ExpirableMap([ 16 | ['a', 1], 17 | ['b', 2] 18 | ]); 19 | 20 | equal(emptyMap.size, 0); 21 | equal(simpleMap.size, 2); 22 | equal(simpleMapWithDefaultTtl.size, 2); 23 | }); 24 | 25 | it('should initialize the map with the given values', (t) => { 26 | const { equal } = tspl(t, { plan: 2 }); 27 | 28 | const map = new ExpirableMap([ 29 | ['a', 1, 10], 30 | ['b', 2, 20] 31 | ]); 32 | 33 | equal(map.get('a'), 1); 34 | equal(map.get('b'), 2); 35 | }); 36 | 37 | it('should set the expiration time for a key', (t) => { 38 | const { equal } = tspl(t, { plan: 2 }); 39 | 40 | const map = new ExpirableMap(); 41 | map.set('a', 1, 10); 42 | 43 | equal(map.size, 1); 44 | equal(map.get('a'), 1); 45 | }); 46 | 47 | it('should remove the key after the expiration time', async (t) => { 48 | const { equal } = tspl(t, { plan: 4 }); 49 | 50 | const map = new ExpirableMap(); 51 | map.set('a', 1, 10); 52 | 53 | equal(map.size, 1); 54 | equal(map.get('a'), 1); 55 | 56 | await sleep(20); 57 | 58 | equal(map.size, 0); 59 | equal(map.get('a'), undefined); 60 | }); 61 | 62 | it('should keep the key if the expiration time is 0', async (t) => { 63 | const { equal } = tspl(t, { plan: 6 }); 64 | 65 | const map = new ExpirableMap(); 66 | 67 | map.set('a', 1, 0); 68 | equal(map.size, 1); 69 | equal(map.get('a'), 1); 70 | 71 | map.set('a', 1, 30); 72 | equal(map.size, 1); 73 | equal(map.get('a'), 1); 74 | 75 | await sleep(20); 76 | 77 | equal(map.size, 1); 78 | equal(map.get('a'), 1); 79 | }); 80 | 81 | it('should remove the key when set twice without keepAlive and time passed', async (t) => { 82 | const { equal } = tspl(t, { plan: 6 }); 83 | 84 | const map = new ExpirableMap([], { defaultTtl: 100, keepAlive: false }); 85 | 86 | map.set('a', 1); 87 | equal(map.size, 1); 88 | equal(map.get('a'), 1); 89 | 90 | map.set('a', 1, 3000); 91 | equal(map.size, 1); 92 | equal(map.get('a'), 1); 93 | 94 | await sleep(500); 95 | 96 | equal(map.size, 0); 97 | equal(map.get('a'), undefined); 98 | }); 99 | 100 | it('should maintain the key when fristly set with time and then set with 0', async (t) => { 101 | const { equal } = tspl(t, { plan: 6 }); 102 | 103 | const map = new ExpirableMap(); 104 | 105 | map.set('a', 1, 10); 106 | equal(map.size, 1); 107 | equal(map.get('a'), 1); 108 | 109 | map.set('a', 1, 0); 110 | equal(map.size, 1); 111 | equal(map.get('a'), 1); 112 | 113 | await sleep(20); 114 | 115 | equal(map.size, 1); 116 | equal(map.get('a'), 1); 117 | }); 118 | 119 | it('should initialize with the given entries and expiration times', async (t) => { 120 | const { equal } = tspl(t, { plan: 9 }); 121 | 122 | const map = new ExpirableMap([ 123 | ['a', 1, 10], 124 | ['b', 2, 20] 125 | ]); 126 | 127 | equal(map.size, 2); 128 | equal(map.get('a'), 1); 129 | equal(map.get('b'), 2); 130 | 131 | await sleep(15); 132 | 133 | equal(map.size, 1); 134 | equal(map.get('a'), undefined); 135 | equal(map.get('b'), 2); 136 | 137 | await sleep(15); 138 | 139 | equal(map.size, 0); 140 | equal(map.get('a'), undefined); 141 | equal(map.get('b'), undefined); 142 | }); 143 | 144 | it('should clear the timeout in setExpiration if the timeout is already set ', async (t) => { 145 | const { equal } = tspl(t, { plan: 1 }); 146 | 147 | const map = new ExpirableMap(); 148 | map.set('a', 20, 100); 149 | map.setExpiration('a', 20); 150 | 151 | await sleep(20); 152 | 153 | equal(map.size, 0); 154 | }); 155 | 156 | it('should return undefined if the map has no the key', async (t) => { 157 | const { equal } = tspl(t, { plan: 1 }); 158 | 159 | const map = new ExpirableMap(); 160 | 161 | equal(map.setExpiration('a', 1), undefined); 162 | }); 163 | 164 | it('should unref timeouts', async (t) => { 165 | const { equal } = tspl(t, { plan: 2 }); 166 | 167 | const map = new ExpirableMap([], { unrefTimeouts: true }); 168 | 169 | map.set('a', 20, 10); 170 | 171 | await sleep(11); 172 | 173 | equal(map.size, 0); 174 | equal(map.get('a'), undefined); 175 | }); 176 | 177 | it('should throw if ttl is not a number', async (t) => { 178 | const { throws } = tspl(t, { plan: 1 }); 179 | 180 | const map = new ExpirableMap(); 181 | 182 | throws( 183 | () => { 184 | // @ts-expect-error - we're testing this 185 | map.set('a', 10, true); 186 | }, 187 | { message: 'TTL must be a number' } 188 | ); 189 | }); 190 | 191 | it('should never expire', async (t) => { 192 | const { equal } = tspl(t, { plan: 6 }); 193 | 194 | const map = new ExpirableMap(); 195 | 196 | map.set('a', 1, 20); 197 | map.set('b', 2); 198 | 199 | equal(map.size, 2); 200 | 201 | await sleep(21); 202 | 203 | equal(map.size, 1); 204 | equal(map.get('a'), undefined); 205 | equal(map.get('b'), 2); 206 | 207 | map.setExpiration('b', 0); 208 | 209 | await sleep(20); 210 | 211 | equal(map.size, 1); 212 | equal(map.get('b'), 2); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /test/queue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { tspl } from '@matteo.collina/tspl'; 3 | import { ExpirableQueue } from '../src/Queue/index.js'; 4 | import { sleep } from '../src/utils/index.js'; 5 | 6 | describe('ExpirableQueue', async () => { 7 | it('should create a queue', (t) => { 8 | const { equal } = tspl(t, { plan: 4 }); 9 | 10 | const emptyQueue = new ExpirableQueue(); 11 | const simpleQueue = new ExpirableQueue([1, 2, 3]); 12 | const simpleQueueWithTtl = new ExpirableQueue([1, 2, 3], { defaultTtl: 10 }); 13 | const complexQueue = new ExpirableQueue([[1, 10], 3, [4]]); 14 | 15 | equal(emptyQueue.size, 0); 16 | equal(simpleQueue.size, 3); 17 | equal(simpleQueueWithTtl.size, 3); 18 | equal(complexQueue.size, 3); 19 | }); 20 | 21 | it('should act as a queue if defaultTtl is 0', (t) => { 22 | const { equal } = tspl(t, { plan: 4 }); 23 | 24 | const queue = new ExpirableQueue([1, 2, 3]); 25 | 26 | equal(queue.dequeue(), 1); 27 | equal(queue.dequeue(), 2); 28 | equal(queue.dequeue(), 3); 29 | equal(queue.dequeue(), undefined); 30 | }); 31 | 32 | it('should return the correct next element', (t) => { 33 | const { equal } = tspl(t, { plan: 1 }); 34 | 35 | const queue = new ExpirableQueue([1, 2, 3]); 36 | 37 | equal(queue.next.value, 1); 38 | }); 39 | 40 | it('should remove the first element after the expiration time', async (t) => { 41 | const { equal } = tspl(t, { plan: 3 }); 42 | 43 | const queue = new ExpirableQueue([1, 2, 3], { defaultTtl: 10 }); 44 | 45 | equal(queue.dequeue(), 1); 46 | equal(queue.dequeue(), 2); 47 | 48 | await sleep(20); 49 | 50 | equal(queue.dequeue(), undefined); 51 | }); 52 | 53 | it('should set an expirable entry and remote it after the expiration time', async (t) => { 54 | const { equal } = tspl(t, { plan: 4 }); 55 | 56 | const queue = new ExpirableQueue([1, 2, 3], { defaultTtl: 10 }); 57 | 58 | equal(queue.size, 3); 59 | await sleep(20); 60 | equal(queue.size, 0); 61 | queue.enqueue(4, 30); 62 | await sleep(20); 63 | equal(queue.size, 1); 64 | await sleep(20); 65 | equal(queue.size, 0); 66 | }); 67 | 68 | it('should thrown if ttl is not a number', (t) => { 69 | const { throws } = tspl(t, { plan: 1 }); 70 | 71 | const queue = new ExpirableQueue(); 72 | 73 | throws(() => queue.enqueue(1, '10' as any)); 74 | }); 75 | 76 | it('should not expire entries if ttl is 0', async (t) => { 77 | const { equal } = tspl(t, { plan: 2 }); 78 | 79 | const queue = new ExpirableQueue(); 80 | const key = queue.enqueue(1); 81 | equal(queue.size, 1); 82 | 83 | queue.setExpiration(key, 0); 84 | 85 | await sleep(10); 86 | 87 | equal(queue.size, 1); 88 | }); 89 | 90 | it('should return the queue if the element is not findable in setExpiration', (t) => { 91 | const { equal } = tspl(t, { plan: 1 }); 92 | 93 | const queue = new ExpirableQueue(); 94 | queue.enqueue(1); 95 | const key = Symbol('random key'); 96 | 97 | equal(queue.setExpiration(key, 10), queue); 98 | }); 99 | 100 | it('should set the expiration unrefering the timer', async (t) => { 101 | const { equal } = tspl(t, { plan: 2 }); 102 | 103 | const queue = new ExpirableQueue([1, 2, 3], { defaultTtl: 10, unrefTimeouts: true }); 104 | 105 | equal(queue.size, 3); 106 | await sleep(20); 107 | 108 | equal(queue.size, 0); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/set.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { tspl } from '@matteo.collina/tspl'; 3 | import { ExpirableSet } from '../src/Set/index.js'; 4 | import { sleep } from '../src/utils/index.js'; 5 | 6 | describe('ExpirableSet', async () => { 7 | it('should create a set', (t) => { 8 | const { equal } = tspl(t, { plan: 4 }); 9 | 10 | const emptySet = new ExpirableSet(); 11 | const simpleSet = new ExpirableSet(['a', 'b']); 12 | const simpleSetWithTtl = new ExpirableSet([10, 20], { defaultTtl: 10 }); 13 | const complexSet = new ExpirableSet([['a', 10], 10, [1]]); 14 | 15 | equal(emptySet.size, 0); 16 | equal(simpleSet.size, 2); 17 | equal(simpleSetWithTtl.size, 2); 18 | equal(complexSet.size, 3); 19 | }); 20 | 21 | it('shoud initialize the set with the given values', (t) => { 22 | const { equal } = tspl(t, { plan: 3 }); 23 | 24 | const set = new ExpirableSet(['a', 'b']); 25 | equal(set.size, 2); 26 | equal(set.has('a'), true); 27 | equal(set.has('b'), true); 28 | }); 29 | 30 | it('should remove the key after the expiration time', async (t) => { 31 | const { equal } = tspl(t, { plan: 4 }); 32 | 33 | const set = new ExpirableSet(['a'], { defaultTtl: 10, keepAlive: true }); 34 | 35 | equal(set.size, 1); 36 | equal(set.has('a'), true); 37 | 38 | await sleep(15); 39 | 40 | equal(set.size, 0); 41 | equal(set.has('a'), false); 42 | }); 43 | 44 | it('should keep they alive when re-set before expiring', async (t) => { 45 | const { equal } = tspl(t, { plan: 6 }); 46 | 47 | const set = new ExpirableSet(['a'], { defaultTtl: 10, keepAlive: true }); 48 | 49 | equal(set.size, 1); 50 | equal(set.has('a'), true); 51 | 52 | set.add('a', 30); 53 | 54 | equal(set.size, 1); 55 | equal(set.has('a'), true); 56 | 57 | await sleep(20); 58 | 59 | equal(set.size, 1); 60 | equal(set.has('a'), true); 61 | }); 62 | 63 | it('should remove the key when twice withouth keepAlive and time passed', async (t) => { 64 | const { equal } = tspl(t, { plan: 6 }); 65 | 66 | const set = new ExpirableSet(['a'], { defaultTtl: 20, keepAlive: false }); 67 | 68 | equal(set.size, 1); 69 | equal(set.has('a'), true); 70 | 71 | set.add('a'); 72 | 73 | equal(set.size, 1); 74 | equal(set.has('a'), true); 75 | 76 | await sleep(25); 77 | 78 | equal(set.size, 0); 79 | equal(set.has('a'), false); 80 | }); 81 | 82 | it('should maintain the key when firstly set with time and then set with 0', async (t) => { 83 | const { equal } = tspl(t, { plan: 6 }); 84 | const set = new ExpirableSet(['a'], { defaultTtl: 10, keepAlive: true }); 85 | 86 | equal(set.size, 1); 87 | equal(set.has('a'), true); 88 | 89 | set.add('a', 0); 90 | 91 | equal(set.size, 1); 92 | equal(set.has('a'), true); 93 | 94 | await sleep(20); 95 | 96 | equal(set.size, 1); 97 | equal(set.has('a'), true); 98 | }); 99 | 100 | it('should initialize the map with the given entries and expiration time', async (t) => { 101 | const { equal } = tspl(t, { plan: 9 }); 102 | 103 | const set = new ExpirableSet([ 104 | ['a', 10], 105 | ['b', 20] 106 | ]); 107 | 108 | equal(set.size, 2); 109 | equal(set.has('a'), true); 110 | equal(set.has('b'), true); 111 | 112 | await sleep(15); 113 | 114 | equal(set.size, 1); 115 | equal(set.has('a'), false); 116 | equal(set.has('b'), true); 117 | 118 | await sleep(10); 119 | 120 | equal(set.size, 0); 121 | equal(set.has('a'), false); 122 | equal(set.has('b'), false); 123 | }); 124 | 125 | it('should not expire', async (t) => { 126 | const { equal } = tspl(t, { plan: 4 }); 127 | 128 | const set = new ExpirableSet([1, 2, 3]); 129 | 130 | set.setExpiration(1, 0); 131 | set.setExpiration(2, 10); 132 | set.setExpiration(3, 0); 133 | 134 | await sleep(20); 135 | 136 | equal(set.size, 2); 137 | equal(set.has(1), true); 138 | equal(set.has(3), true); 139 | equal(set.has(2), false); 140 | }); 141 | 142 | it('should set the expiration unrefering the timeout', async (t) => { 143 | const { equal, } = tspl(t, { plan: 2 }); 144 | 145 | const set = new ExpirableSet([1], { unrefTimeouts: true }); 146 | 147 | set.setExpiration(1, 10); 148 | 149 | equal(set.size, 1); 150 | 151 | await sleep(15); 152 | 153 | equal(set.has(1), false); 154 | }); 155 | 156 | it('should thrown if ttl is not a number', (t) => { 157 | const { throws } = tspl(t, { plan: 1 }); 158 | 159 | const set = new ExpirableSet(); 160 | 161 | throws(() => { 162 | // @ts-expect-error - we're testing this 163 | set.add(1, 'kaboom'); 164 | }, { message: 'TTL must be a number' }); 165 | }) 166 | }); 167 | -------------------------------------------------------------------------------- /test/stack.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import { tspl } from '@matteo.collina/tspl'; 3 | import { ExpirableStack } from '../src/Stack/index.js'; 4 | import { sleep } from '../src/utils/index.js'; 5 | 6 | describe('ExpirableStack', async () => { 7 | it('should create a stack', (t) => { 8 | const { equal } = tspl(t, { plan: 5 }); 9 | 10 | const emptyStack = new ExpirableStack(); 11 | const simpleStack = new ExpirableStack([1, 2, 3]); 12 | const complexStackWithoutTtl = new ExpirableStack([[1], 2, [3]]); 13 | const complexStackWithTtl = new ExpirableStack([[1, 10], 2, [3, 20]]); 14 | const stackWithObjects = new ExpirableStack([{ a: 1 }, { b: 2 }]); 15 | 16 | equal(emptyStack.size, 0); 17 | equal(simpleStack.size, 3); 18 | equal(complexStackWithoutTtl.size, 3); 19 | equal(complexStackWithTtl.size, 3); 20 | equal(stackWithObjects.size, 2); 21 | }); 22 | 23 | it('should act as stack if defaultTtl is 0', (t) => { 24 | const { equal } = tspl(t, { plan: 4 }); 25 | 26 | const stack = new ExpirableStack([1, 2, 3]); 27 | 28 | equal(stack.pop(), 1); 29 | equal(stack.pop(), 2); 30 | equal(stack.pop(), 3); 31 | equal(stack.pop(), undefined); 32 | }); 33 | 34 | it('should return the correct next element', (t) => { 35 | const { equal } = tspl(t, { plan: 1 }); 36 | 37 | const stack = new ExpirableStack([1, 2, 3]); 38 | 39 | equal(stack.next, 3); 40 | }); 41 | 42 | it('should remove the first element after the expiration time', async (t) => { 43 | const { equal } = tspl(t, { plan: 3 }); 44 | 45 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 46 | 47 | equal(stack.pop(), 1); 48 | equal(stack.pop(), 2); 49 | await sleep(20); 50 | equal(stack.pop(), undefined); 51 | }); 52 | 53 | it('should set an expirable entry and remove it after the expiration time', async (t) => { 54 | const { equal } = tspl(t, { plan: 5 }); 55 | 56 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 57 | 58 | equal(stack.size, 3); 59 | await sleep(20); 60 | equal(stack.size, 0); 61 | stack.push(4, 30); 62 | equal(stack.size, 1); 63 | await sleep(20); 64 | equal(stack.size, 1); 65 | await sleep(20); 66 | equal(stack.size, 0); 67 | }); 68 | 69 | it('should throw an error if the ttl is not a number', (t) => { 70 | const { throws } = tspl(t, { plan: 1 }); 71 | 72 | const stack = new ExpirableStack(); 73 | 74 | throws( 75 | () => { 76 | stack.push(1, '10' as any); 77 | }, 78 | { message: 'TTL must be a number' } 79 | ); 80 | }); 81 | 82 | it('should return nothing if the element popped is undefined', (t) => { 83 | const { equal } = tspl(t, { plan: 1 }); 84 | 85 | const stack = new ExpirableStack(); 86 | 87 | equal(stack.pop(), undefined); 88 | }); 89 | 90 | it('should clear the timeout in setExpiration if the timeout is already set ', async (t) => { 91 | const { equal } = tspl(t, { plan: 1 }); 92 | 93 | const key = Symbol('key'); 94 | 95 | const stack = new ExpirableStack(); 96 | stack.setExpiration(key, 100); 97 | stack.setExpiration(key, 20); 98 | 99 | await sleep(20); 100 | 101 | equal(stack.size, 0); 102 | }); 103 | 104 | it('should unref the timeout if passed in options', async (t) => { 105 | const { equal } = tspl(t, { plan: 1 }); 106 | 107 | const stack = new ExpirableStack([1, 2, 3], { 108 | defaultTtl: 10, 109 | unrefTimeouts: true 110 | }); 111 | 112 | await sleep(20); 113 | 114 | equal(stack.size, 0); 115 | }); 116 | 117 | it('should next return undefined if the length is less or equal 0', (t) => { 118 | const { equal } = tspl(t, { plan: 1 }); 119 | 120 | const stack = new ExpirableStack(); 121 | 122 | equal(stack.next, undefined); 123 | }); 124 | 125 | it('should never expire', async (t) => { 126 | const { equal } = tspl(t, { plan: 3 }); 127 | 128 | const stack = new ExpirableStack(); 129 | const key = Symbol('b'); 130 | 131 | stack.push('a', 10); 132 | stack.push(key); 133 | 134 | equal(stack.size, 2); 135 | 136 | await sleep(11); 137 | 138 | equal(stack.size, 1); 139 | 140 | stack.setExpiration(key, 0); 141 | await sleep(20); 142 | 143 | equal(stack.size, 1); 144 | }); 145 | }); 146 | 147 | describe('ExpirableStack hooks', () => { 148 | it('should call the beforeExpire hook before an entry expires', async (t) => { 149 | const { equal } = tspl(t, { plan: 3 }); 150 | 151 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 152 | 153 | let i = 0; 154 | stack.addHook('beforeExpire', (value) => { 155 | i++; 156 | equal(value, i); 157 | }); 158 | 159 | await sleep(20); 160 | }); 161 | 162 | it('should call the afterExpire hook after an entry expires', async (t) => { 163 | const { equal } = tspl(t, { plan: 3 }); 164 | 165 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 166 | 167 | let i = 0; 168 | stack.addHook('afterExpire', (value) => { 169 | i++; 170 | equal(value, i); 171 | }); 172 | 173 | await sleep(20); 174 | }); 175 | 176 | it('should throw an error if the hook is not valid', (t) => { 177 | const { throws } = tspl(t, { plan: 1 }); 178 | 179 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 180 | 181 | throws(() => { 182 | stack.addHook('invalidHook' as any, () => ({})); 183 | }); 184 | }); 185 | 186 | it('should throw if the hook does not exist', (t) => { 187 | const { throws } = tspl(t, { plan: 1 }); 188 | 189 | const stack = new ExpirableStack([1, 2, 3], { defaultTtl: 10 }); 190 | 191 | throws(() => { 192 | stack.runHook('invalidHook' as any); 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "esnext", 5 | "lib": ["esnext", "dom"], 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "removeComments": true, 11 | "esModuleInterop": true, 12 | "outDir": "dist" 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "dist", 17 | "packages" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------