From b80e053978f6717cfbfd525f6c7a4f034f07f87c Mon Sep 17 00:00:00 2001 From: Uladzimir Kasacheuski Date: Thu, 26 Dec 2024 13:27:08 -0800 Subject: [PATCH] feat(terms): use uni-time glossary for duration term (#9) --- package-lock.json | 88 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 4 ++- src/cache.test.ts | 28 +++------------ src/cache.ts | 43 +++++++---------------- 4 files changed, 103 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc01587..cb5e913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.3.3", "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@ehmpathy/uni-time": "^1.7.4" + }, "devDependencies": { "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "13.1.0", @@ -33,7 +36,6 @@ "prettier": "2.8.1", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "type-fns": "^1.19.0", "typescript": "5.4.5" }, "engines": { @@ -2109,7 +2111,6 @@ }, "node_modules/@ehmpathy/error-fns": { "version": "1.0.2", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2121,11 +2122,35 @@ }, "node_modules/@ehmpathy/error-fns/node_modules/type-fns": { "version": "0.9.0", - "dev": true, "engines": { "node": ">=8.0.0" } }, + "node_modules/@ehmpathy/uni-time": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@ehmpathy/uni-time/-/uni-time-1.7.4.tgz", + "integrity": "sha512-CU9RajidPqAc4OUf8sVf1aM8+0lDCYdPSBwpUS8bMFUU9PIoOPCllyDVx5dh0Zl3j0MpkuHE1/HKSqoQHI8Pzw==", + "hasInstallScript": true, + "dependencies": { + "@ehmpathy/error-fns": "1.0.2", + "date-fns": "3.6.0", + "domain-glossaries": "1.0.0", + "test-fns": "1.5.0", + "type-fns": "1.19.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@ehmpathy/uni-time/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -8853,6 +8878,15 @@ "node": ">=0.10.0" } }, + "node_modules/domain-glossaries": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/domain-glossaries/-/domain-glossaries-1.0.0.tgz", + "integrity": "sha512-veAWouAHm93KQtR3v0lt0Ulnfm5GHmeY9H9A3wN4uY6GkKgRMIYYAjKMErPp0oetifsPXFbkr+KVFPgE7HrvnQ==", + "hasInstallScript": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/domain-objects": { "version": "0.22.1", "resolved": "https://registry.npmjs.org/domain-objects/-/domain-objects-0.22.1.tgz", @@ -17863,6 +17897,39 @@ "node": ">=8" } }, + "node_modules/test-fns": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/test-fns/-/test-fns-1.5.0.tgz", + "integrity": "sha512-esvhi+y5tQaD5iuGlFqS82YcrmKPqoJEq7m6YFjewIFGOJTOd4GCYFo/uq50oQP5kxmHSMvmJq1GHyu3BGW5oA==", + "hasInstallScript": true, + "dependencies": { + "@ehmpathy/error-fns": "1.3.1", + "uuid": "10.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/test-fns/node_modules/@ehmpathy/error-fns": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", + "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "hasInstallScript": true, + "dependencies": { + "type-fns": "0.9.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/test-fns/node_modules/type-fns": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", + "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -18406,7 +18473,6 @@ "version": "1.19.0", "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-1.19.0.tgz", "integrity": "sha512-aJsH1CuJVI/mBrwI1fPJ7gei0HLp6tmXxWYAd9FtCNJsHd2RwjWtjjS66GXJxLUMOUMdcGvsTmrWDZAcI0IU0A==", - "dev": true, "hasInstallScript": true, "dependencies": { "@ehmpathy/error-fns": "1.0.2", @@ -18419,7 +18485,6 @@ }, "node_modules/type-fns/node_modules/type-fns": { "version": "1.16.0", - "dev": true, "dependencies": { "@ehmpathy/error-fns": "1.0.2" }, @@ -18429,7 +18494,6 @@ }, "node_modules/type-fns/node_modules/uuid": { "version": "9.0.0", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -18672,6 +18736,18 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, diff --git a/package.json b/package.json index ab17ec3..15e390a 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,14 @@ "prettier": "2.8.1", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "type-fns": "^1.19.0", "typescript": "5.4.5" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } + }, + "dependencies": { + "@ehmpathy/uni-time": "^1.7.4" } } diff --git a/src/cache.test.ts b/src/cache.test.ts index 1dc45e1..2c262f2 100644 --- a/src/cache.test.ts +++ b/src/cache.test.ts @@ -21,25 +21,7 @@ describe('cache', () => { expect(licks).toEqual(3); }); it('should respect the default expiration for the cache', async () => { - const { set, get } = createCache({ defaultSecondsUntilExpiration: 10 }); // we're gonna use this cache to keep track of the popcorn in the microwave - we should check more regularly since it changes quickly! - set('how popped is the popcorn?', 'not popped'); - - // prove that we recorded the value and its accessible immediately after setting - const popcornStatus = get('how popped is the popcorn?'); - expect(popcornStatus).toEqual('not popped'); - - // prove that the value is still accessible after 9 seconds, since default ttl is 10 seconds - await sleep(9 * 1000); - const popcornStatusAfter9Sec = get('how popped is the popcorn?'); - expect(popcornStatusAfter9Sec).toEqual('not popped'); // still should say not popped - - // and prove that after a total of 9 seconds, the status is no longer in the cache - await sleep(1 * 1000); // sleep 1 more second - const popcornStatusAfter10Sec = get('how popped is the popcorn?'); - expect(popcornStatusAfter10Sec).toEqual(undefined); // no longer defined, since the default seconds until expiration was 15 - }); - it('should respect the default expiration for the cache set by shorthand alias', async () => { - const { set, get } = createCache({ seconds: 10 }); // we're gonna use this cache to keep track of the popcorn in the microwave - we should check more regularly since it changes quickly! + const { set, get } = createCache({ expiration: { seconds: 10 } }); // we're gonna use this cache to keep track of the popcorn in the microwave - we should check more regularly since it changes quickly! set('how popped is the popcorn?', 'not popped'); // prove that we recorded the value and its accessible immediately after setting @@ -58,7 +40,7 @@ describe('cache', () => { }); it('should respect the item level expiration for the cache', async () => { const { set, get } = createCache(); // remember, default expiration is greater than 1 min - set('ice cream state', 'solid', { secondsUntilExpiration: 5 }); // ice cream changes quickly in the heat! lets keep a quick eye on this + set('ice cream state', 'solid', { expiration: { seconds: 5 } }); // ice cream changes quickly in the heat! lets keep a quick eye on this // prove that we recorded the value and its accessible immediately after setting const iceCreamState = get('ice cream state'); @@ -74,9 +56,9 @@ describe('cache', () => { const iceCreamStateAfter5Sec = get('ice cream state'); expect(iceCreamStateAfter5Sec).toEqual(undefined); // no longer defined, since the item level seconds until expiration was 5 }); - it('should consider secondsUntilExpiration of null or infinity as never expiring', async () => { + it('should consider secondsUntilExpiration of null as never expiring', async () => { const { set, get } = createCache({ - defaultSecondsUntilExpiration: 0, // expire immediately + expiration: { seconds: 0 }, // expire immediately }); // prove that setting something to the cache with default state will have it expired immediately @@ -86,7 +68,7 @@ describe('cache', () => { // prove that if we record the memory with expires-at Infinity, it persists await set('elephant-memory', 'something', { - secondsUntilExpiration: Infinity, + expiration: null, }); const elephantMemory = await get('elephant-memory'); expect(elephantMemory).toEqual('something'); diff --git a/src/cache.ts b/src/cache.ts index 7262bbb..993130b 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,13 +1,11 @@ -import type { PickOne } from 'type-fns'; +import { toMilliseconds, UniDuration } from '@ehmpathy/uni-time'; export interface SimpleInMemoryCache { get: (key: string) => T | undefined; set: ( key: string, value: T, - options?: Partial< - PickOne<{ secondsUntilExpiration?: number; seconds?: number }> - >, + options?: { expiration?: UniDuration | null }, ) => void; keys: () => string[]; } @@ -18,29 +16,13 @@ export interface SimpleInMemoryCacheState { const getMseNow = () => new Date().getTime(); -export const createCache = ({ - seconds, - defaultSecondsUntilExpiration: defaultSecondsUntilExpirationInput, -}: Partial< - PickOne<{ - /** - * the number of seconds items in the cache expire after - */ - defaultSecondsUntilExpiration?: number; - - /** - * a shorthand alias for `defaultSecondsUntilExpiration` - * - * note - * - if both options are set, `defaultSecondsUntilExpirationInput` takes precedence - */ - seconds?: number; - }> -> = {}): SimpleInMemoryCache => { - // resolve input alias - const defaultSecondsUntilExpiration = - defaultSecondsUntilExpirationInput ?? seconds ?? 5 * 60; - +export const createCache = ( + { + expiration: defaultExpiration, + }: { + expiration?: UniDuration | null; + } = { expiration: { minutes: 5 } }, +): SimpleInMemoryCache => { // initialize a fresh in-memory cache object const cache: SimpleInMemoryCacheState = {}; @@ -49,8 +31,8 @@ export const createCache = ({ key: string, value: T | undefined, { - secondsUntilExpiration = defaultSecondsUntilExpiration, - }: { secondsUntilExpiration?: number } = {}, + expiration = defaultExpiration, + }: { expiration?: UniDuration | null } = {}, ) => { // handle cache invalidation if (value === undefined) { @@ -59,7 +41,8 @@ export const createCache = ({ } // handle setting - const expiresAtMse = getMseNow() + secondsUntilExpiration * 1000; + const expiresAtMse = + getMseNow() + (expiration ? toMilliseconds(expiration) : Infinity); // infinity if null cache[key] = { value, expiresAtMse }; };