pax_global_header00006660000000000000000000000064141110025120014475gustar00rootroot0000000000000052 comment=0f9f8631510815dd7f567372461fa923c25f2c46 map-age-cleaner-0.2.0/000077500000000000000000000000001411100251200144125ustar00rootroot00000000000000map-age-cleaner-0.2.0/.editorconfig000066400000000000000000000002571411100251200170730ustar00rootroot00000000000000root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2 map-age-cleaner-0.2.0/.gitattributes000066400000000000000000000000231411100251200173000ustar00rootroot00000000000000* text=auto eol=lf map-age-cleaner-0.2.0/.github/000077500000000000000000000000001411100251200157525ustar00rootroot00000000000000map-age-cleaner-0.2.0/.github/funding.yml000066400000000000000000000000361411100251200201260ustar00rootroot00000000000000tidelift: npm/map-age-cleaner map-age-cleaner-0.2.0/.github/security.md000066400000000000000000000002631411100251200201440ustar00rootroot00000000000000# Security Policy To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. map-age-cleaner-0.2.0/.github/workflows/000077500000000000000000000000001411100251200200075ustar00rootroot00000000000000map-age-cleaner-0.2.0/.github/workflows/main.yaml000066400000000000000000000012421411100251200216160ustar00rootroot00000000000000name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: - 14 - 12 - 10 - 8 - 7.6 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test - run: ./node_modules/.bin/nyc report --reporter=text-lcov > coverage.lcov - uses: codecov/codecov-action@v1 with: file: coverage.lcov name: ${{ matrix.node-version }} map-age-cleaner-0.2.0/.gitignore000066400000000000000000000000611411100251200163770ustar00rootroot00000000000000dist node_modules yarn.lock .nyc_output coverage map-age-cleaner-0.2.0/.npmrc000066400000000000000000000000231411100251200155250ustar00rootroot00000000000000package-lock=false map-age-cleaner-0.2.0/license000066400000000000000000000021431411100251200157570ustar00rootroot00000000000000MIT License Copyright (c) Sam Verschueren (github.com/SamVerschueren) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. map-age-cleaner-0.2.0/package.json000066400000000000000000000022571411100251200167060ustar00rootroot00000000000000{ "name": "map-age-cleaner", "version": "0.2.0", "description": "Automatically cleanup expired items in a Map", "license": "MIT", "repository": "SamVerschueren/map-age-cleaner", "author": { "name": "Sam Verschueren", "email": "sam.verschueren@gmail.com", "url": "github.com/SamVerschueren" }, "main": "dist/index.js", "engines": { "node": ">=7.6" }, "scripts": { "prepublishOnly": "npm run build", "pretest": "npm run build -- --sourceMap", "test": "npm run lint && nyc ava dist/test.js", "lint": "tslint --format stylish --project .", "build": "npm run clean && tsc", "clean": "del-cli dist" }, "files": [ "dist/index.js", "dist/index.d.ts" ], "keywords": [ "map", "age", "cleaner", "maxage", "expire", "expiration", "expiring" ], "dependencies": { "p-defer": "^1.0.0" }, "devDependencies": { "@types/delay": "^2.0.1", "@types/node": "^10.7.1", "ava": "^0.25.0", "codecov": "^3.0.0", "del-cli": "^1.1.0", "delay": "^3.0.0", "nyc": "^12.0.0", "tslint": "^5.11.0", "tslint-xo": "^0.9.0", "typescript": "^3.0.1" }, "typings": "dist/index.d.ts", "sideEffects": false, "nyc": { "exclude": [ "dist/test.js" ] } } map-age-cleaner-0.2.0/readme.md000066400000000000000000000035131411100251200161730ustar00rootroot00000000000000# map-age-cleaner ![CI](https://github.com/SamVerschueren/map-age-cleaner/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/SamVerschueren/map-age-cleaner/badge.svg?branch=master)](https://codecov.io/gh/SamVerschueren/map-age-cleaner?branch=master) > Automatically cleanup expired items in a Map ## Install ``` $ npm install map-age-cleaner ``` ## Usage ```js import mapAgeCleaner = require('map-age-cleaner'); const map = new Map([ ['unicorn', {data: '🦄', maxAge: Date.now() + 1000}] ]); mapAgeCleaner(map); map.has('unicorn'); //=> true // Wait for 1 second... map.has('unicorn'); //=> false ``` > **Note**: Items have to be ordered ascending based on the expiry property. This means that the item which will be expired first, should be in the first position of the `Map`. ## API ### mapAgeCleaner(map, [property]) Returns the `Map` instance. #### map Type: `Map` Map instance which should be cleaned up. #### property Type: `string`
Default: `maxAge` Name of the property which olds the expiry timestamp. ## Related - [expiry-map](https://github.com/SamVerschueren/expiry-map) - A `Map` implementation with expirable items - [expiry-set](https://github.com/SamVerschueren/expiry-set) - A `Set` implementation with expirable keys - [mem](https://github.com/sindresorhus/mem) - Memoize functions ## License MIT © [Sam Verschueren](https://github.com/SamVerschueren) ---
Get professional support for this package with a Tidelift subscription
Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies.
map-age-cleaner-0.2.0/source/000077500000000000000000000000001411100251200157125ustar00rootroot00000000000000map-age-cleaner-0.2.0/source/index.ts000066400000000000000000000063311411100251200173740ustar00rootroot00000000000000import pDefer = require('p-defer'); interface Entry { [key: string]: any; } interface MaxAgeEntry extends Entry { maxAge: number; } interface DeferredPromise { promise: Promise; resolve(value?: T): void; reject(error?: E): void; } /** * Automatically cleanup the items in the provided `map`. The property of the expiration timestamp should be named `maxAge`. * * @param map - Map instance which should be cleaned up. */ function mapAgeCleaner(map: Map); /** * Automatically cleanup the items in the provided `map`. * * @param map - Map instance which should be cleaned up. * @param property - Name of the property which olds the expiry timestamp. */ function mapAgeCleaner(map: Map, property: string); function mapAgeCleaner(map: Map, property = 'maxAge') { let processingKey: K | undefined; let processingTimer: NodeJS.Timer | undefined; let processingDeferred: DeferredPromise | undefined; const cleanup = async () => { if (processingKey !== undefined) { // If we are already processing an item, we can safely exit return; } const setupTimer = async (item: [K, V]) => { processingDeferred = pDefer() as DeferredPromise; const delay = (item[1] as any)[property] - Date.now(); if (delay <= 0) { // Remove the item immediately if the delay is equal to or below 0 map.delete(item[0]); processingDeferred.resolve(); return; } // Keep track of the current processed key processingKey = item[0]; processingTimer = setTimeout(() => { // Remove the item when the timeout fires map.delete(item[0]); if (processingDeferred) { processingDeferred.resolve(); } }, delay); // tslint:disable-next-line:strict-type-predicates if (typeof processingTimer.unref === 'function') { // Don't hold up the process from exiting processingTimer.unref(); } return processingDeferred.promise; }; try { for (const entry of map) { await setupTimer(entry); } } catch { // Do nothing if an error occurs, this means the timer was cleaned up and we should stop processing } processingKey = undefined; }; const reset = () => { processingKey = undefined; if (processingTimer !== undefined) { clearTimeout(processingTimer); processingTimer = undefined; } if (processingDeferred !== undefined) { // tslint:disable-line:early-exit processingDeferred.reject(undefined); processingDeferred = undefined; } }; const originalSet = map.set.bind(map); map.set = (key: K, value: V) => { if (map.has(key)) { // If the key already exist, remove it so we can add it back at the end of the map. map.delete(key); } // Call the original `map.set` const result = originalSet(key, value); // If we are already processing a key and the key added is the current processed key, stop processing it if (processingKey && processingKey === key) { reset(); } // Always run the cleanup method in case it wasn't started yet cleanup(); // tslint:disable-line:no-floating-promises return result; }; cleanup(); // tslint:disable-line:no-floating-promises return map; } export = mapAgeCleaner; map-age-cleaner-0.2.0/source/test.ts000066400000000000000000000071421411100251200172450ustar00rootroot00000000000000/* tslint:disable:await-promise */ import test from 'ava'; import delay from 'delay'; import mapAgeCleaner = require('.'); interface Context { map: Map; } test.beforeEach(t => { t.context.map = new Map(); }); test('auto removal on initial Map', async t => { const map = new Map([ ['unicorn', {maxAge: Date.now() + 1000, data: '🦄'}] ]); mapAgeCleaner(map); t.true(map.has('unicorn')); await delay(400); t.true(map.has('unicorn')); await delay(605); t.false(map.has('unicorn')); }); test('auto removal', async t => { const {map} = t.context as Context; mapAgeCleaner(map); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄'}); t.true(map.has('unicorn')); await delay(400); t.true(map.has('unicorn')); await delay(605); t.false(map.has('unicorn')); }); test('return map instance', async t => { const map = mapAgeCleaner(new Map([ ['unicorn', {maxAge: Date.now() + 1000, data: '🦄'}] ])); t.true(map.has('unicorn')); await delay(1005); t.false(map.has('unicorn')); }); test('use other property name', async t => { const map = new Map([ ['unicorn', {timestamp: Date.now() + 1000, data: '🦄'}] ]); mapAgeCleaner(map, 'timestamp'); t.true(map.has('unicorn')); await delay(1005); t.false(map.has('unicorn')); }); test('order on reset', async t => { const {map} = t.context as Context; mapAgeCleaner(map); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄'}); await delay(400); map.set('rainbow', {maxAge: Date.now() + 1000, data: '🌈'}); await delay(100); map.set('hooray', {maxAge: Date.now() + 1000, data: '🎉'}); await delay(300); map.set('rainbow', {maxAge: Date.now() + 1000, data: '🌈🦄'}); await delay(205); t.false(map.has('unicorn')); t.true(map.has('rainbow')); t.true(map.has('hooray')); t.is(map.size, 2); await delay(505); t.false(map.has('unicorn')); t.true(map.has('rainbow')); t.false(map.has('hooray')); t.is(map.size, 1); await delay(305); t.false(map.has('unicorn')); t.false(map.has('rainbow')); t.false(map.has('hooray')); t.is(map.size, 0); }); test('reset currently processed item', async t => { const {map} = t.context as Context; mapAgeCleaner(map); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄'}); await delay(200); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄🦄'}); await delay(200); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄🦄🦄'}); await delay(400); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄🦄🦄🦄'}); await delay(300); t.true(map.has('unicorn')); }); test('reset currently processed item and process next', async t => { const {map} = t.context as Context; mapAgeCleaner(map); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄'}); await delay(500); map.set('rainbow', {maxAge: Date.now() + 1000, data: '🌈'}); await delay(200); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄🦄'}); await delay(200); map.set('unicorn', {maxAge: Date.now() + 1000, data: '🦄🦄'}); await delay(400); t.true(map.has('unicorn')); t.true(map.has('rainbow')); t.is(map.size, 2); await delay(205); t.true(map.has('unicorn')); t.false(map.has('rainbow')); t.is(map.size, 1); await delay(405); t.false(map.has('unicorn')); t.false(map.has('rainbow')); t.is(map.size, 0); }); test('cleanup items which have same expiration timestamp', async t => { const map = new Map([ ['unicorn', {maxAge: Date.now() + 1000, data: '🦄'}], ['rainbow', {maxAge: Date.now() + 1000, data: '🌈'}] ]); mapAgeCleaner(map); t.is(map.size, 2); await delay(1005); t.is(map.size, 0); }); map-age-cleaner-0.2.0/tsconfig.json000066400000000000000000000010061411100251200171160ustar00rootroot00000000000000{ "compilerOptions": { "outDir": "dist", "target": "es2017", "lib": [ "es2015" ], "module": "commonjs", "moduleResolution": "node", "declaration": true, "pretty": true, "newLine": "lf", "stripInternal": true, "strict": true, "noImplicitAny": false, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noEmitOnError": true, "forceConsistentCasingInFileNames": true }, "exclude": [ "node_modules", "dist" ] } map-age-cleaner-0.2.0/tslint.json000066400000000000000000000001121411100251200166140ustar00rootroot00000000000000{ "extends": "tslint-xo", "rules": { "no-require-imports": false } }