diff --git a/.gitignore b/.gitignore index 5e295c6..0966137 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cache.json !dist/index.js **/*.d.ts !dist/index.d.ts +!dist/index.js.map diff --git a/CHANGELOG.md b/CHANGELOG.md index a45bc28..e8558e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.2.3](https://github.com/enda-automation/amemo/compare/v1.2.2...v1.2.3) (2024-04-08) + +### [1.2.2](https://github.com/enda-automation/amemo/compare/v1.2.1...v1.2.2) (2024-04-08) + +### [1.2.1](https://github.com/enda-automation/amemo/compare/v1.2.0...v1.2.1) (2024-04-08) + ## 1.2.0 (2024-04-08) diff --git a/dist/index.js b/dist/index.js index 95b4c2e..8dfc062 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,2 +1,2 @@ -var i=Symbol("NotFound"),N=1e3,C=60*N,D=60*C,l=24*D,R=7*l;import*as r from"fs";import*as b from"path";var y=class{constructor(n={}){this.cache=n}get(n,t){let e=this.cache[n];return e?Date.now()-e.timestamp>t?i:e.value:i}set(n,t){this.cache[n]={timestamp:Date.now(),value:t}}save(){}};var p=class extends y{constructor(t={}){super();this.opts=t;this.cacheFile=t.path??"cache.json",this.autoSave=t.autoSave??!0;try{if(!r.existsSync(this.cacheFile))return;let e=r.readFileSync(this.cacheFile,"utf8");super.cache=JSON.parse(e)}catch{}}set(t,e){super.set(t,e),this.autoSave&&this.save()}async save(){for(let[t,e]of Object.entries(this.cache))e.value instanceof Promise&&(this.cache[t].value=await e.value);r.mkdirSync(b.dirname(this.cacheFile),{recursive:!0}),r.writeFileSync(this.cacheFile,JSON.stringify(this.cache))}};function k(s,n,t,e,c,a=""){if(e[a])return e[a];let m=new Proxy(s,{get(f,d){let o=a+"/"+d.toString(),x=f[d];if(typeof x=="function"){if(c[o])return c[o];let F=x.bind(f),{defaultExpire:T=1*l,pathExpire:E={},onHit:O=()=>{},onMiss:P=()=>{}}=t,w=E[o]??T,S=function(...u){let h=o+": "+JSON.stringify(u),v=n.get(h,w);if(v!==i)return O(h,u),v;let g=F(...u);return n.set(h,g),P(h,u),g};return c[o]=S,S}return k(f[d],n,t,e,c,o)}});return e[a]=m,m}function q(s,n={}){let{cacheStore:t=new p}=n;return k(s,t,n,{},{})}export{q as cacheProxy}; +var i=Symbol("NotFound"),N=1e3,C=60*N,j=60*C,m=24*j,D=7*m;import*as r from"fs";import*as g from"path";var f=class{constructor(n={}){this.cache=n}get(n,t){let e=this.cache[n];return e?Date.now()-e.timestamp>t?i:e.value:i}set(n,t){this.cache[n]={timestamp:Date.now(),value:t}}save(){}};var p=class extends f{constructor(t={}){super();this.opts=t;this.cacheFile=t.path??"cache.json",this.autoSave=t.autoSave??!0;try{if(!r.existsSync(this.cacheFile))return;let e=r.readFileSync(this.cacheFile,"utf8");super.cache=JSON.parse(e)}catch{}}set(t,e){super.set(t,e),this.autoSave&&this.save()}async save(){for(let[t,e]of Object.entries(this.cache))e.value instanceof Promise&&(this.cache[t].value=await e.value);r.mkdirSync(g.dirname(this.cacheFile),{recursive:!0}),r.writeFileSync(this.cacheFile,JSON.stringify(this.cache))}};function k(s,n,t,e,c,a=""){if(e[a])return e[a];let x=new Proxy(s,{get(d,l){let o=a+"/"+l.toString(),u=d[l];if(typeof u=="function"){if(c[o])return c[o];let F=u.bind(d),{defaultExpire:O=1*m,pathExpire:T={},onHit:E=()=>{},onMiss:w=()=>{}}=t,P=T[o]??O,S=function(...h){let y=o+": "+JSON.stringify(h),v=n.get(y,P);if(v!==i)return E(y,h),v;let b=F(...h);return n.set(y,b),w(y,h),b};return c[o]=S,S}return u instanceof Object?k(d[l],n,t,e,c,o):u}});return e[a]=x,x}function q(s,n={}){let{cacheStore:t=new p}=n;return k(s,t,n,{},{})}export{q as cacheProxy}; //# sourceMappingURL=index.js.map diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..d81c70b --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/cache-store.ts", "../src/file-cache-store.ts", "../src/mem-cache-store.ts", "../src/cache-proxy.ts"], + "sourcesContent": ["export interface CacheStore {\n get(key: string, expire: number): unknown;\n set(key: string, value: unknown): void;\n save(): void;\n}\n\nexport type Entry = {\n timestamp: number;\n value: unknown;\n};\n\nexport const NotFound = Symbol(\"NotFound\");\n\nexport const SECOND = 1000;\nexport const MINUTE = 60 * SECOND;\nexport const HOUR = 60 * MINUTE;\nexport const DAY = 24 * HOUR;\nexport const WEEK = 7 * DAY;\n", "import * as fs from \"fs\";\nimport * as path from \"path\";\n\nimport { MemCacheStore } from \"./mem-cache-store\";\n\nexport type FileCacheStoreOpts = {\n path?: string;\n autoSave?: boolean;\n};\n\nexport class FileCacheStore extends MemCacheStore {\n public readonly cacheFile: string;\n private readonly autoSave: boolean;\n constructor(public readonly opts: FileCacheStoreOpts = {}) {\n super();\n this.cacheFile = opts.path ?? \"cache.json\";\n this.autoSave = opts.autoSave ?? true;\n try {\n if (!fs.existsSync(this.cacheFile)) {\n return;\n }\n const data = fs.readFileSync(this.cacheFile, \"utf8\");\n super.cache = JSON.parse(data);\n } catch (e) {\n // ignore\n }\n }\n\n set(key: string, value: unknown) {\n super.set(key, value);\n if (this.autoSave) {\n this.save();\n }\n }\n\n async save() {\n for (const [key, entry] of Object.entries(this.cache)) {\n if (entry.value instanceof Promise) {\n this.cache[key].value = await entry.value;\n }\n }\n\n fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });\n fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache));\n }\n}\n", "import { CacheStore, Entry, NotFound } from \"./cache-store\";\n\nexport class MemCacheStore implements CacheStore {\n constructor(protected cache: Record = {}) {}\n\n get(key: string, expire: number) {\n const entry = this.cache[key];\n if (!entry) {\n return NotFound;\n }\n if (Date.now() - entry.timestamp > expire) {\n return NotFound;\n }\n return entry.value;\n }\n\n set(key: string, value: unknown) {\n this.cache[key] = {\n timestamp: Date.now(),\n value,\n };\n }\n\n save() {}\n}\n", "import { CacheStore, DAY, NotFound } from \"./cache-store\";\nimport { FileCacheStore } from \"./file-cache-store\";\n\nexport type CacheProxyOpts = {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onHit?: (key: string, args: any[]) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMiss?: (key: string, args: any[]) => void;\n // Default expiration time in milliseconds\n defaultExpire?: number;\n // Expiration time per property path\n pathExpire?: Record;\n cacheStore?: CacheStore;\n};\n\ntype ProxyCache = Record;\n\nfunction createProxy(\n api: T,\n cache: CacheStore,\n opts: CacheProxyOpts,\n proxyCache: ProxyCache,\n wrapperCache: Record,\n path = \"\",\n): T {\n if (proxyCache[path]) {\n return proxyCache[path];\n }\n\n const proxy = new Proxy(api, {\n get(target, prop /*, receiver*/) {\n const currentPath = path + \"/\" + prop.toString();\n const p = target[prop as keyof T];\n if (typeof p === \"function\") {\n if (wrapperCache[currentPath]) {\n return wrapperCache[currentPath];\n }\n const binded = p.bind(target);\n const {\n defaultExpire = 1 * DAY,\n pathExpire = {},\n onHit = () => {},\n onMiss = () => {},\n } = opts;\n const expire = pathExpire[currentPath] ?? defaultExpire;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const wrapped = function (...args: any[]) {\n const key = currentPath + \": \" + JSON.stringify(args);\n const cached = cache.get(key, expire);\n if (cached !== NotFound) {\n onHit(key, args);\n return cached;\n }\n\n const result = binded(...args);\n cache.set(key, result);\n onMiss(key, args);\n return result;\n };\n wrapperCache[currentPath] = wrapped;\n return wrapped;\n }\n\n if (!(p instanceof Object)) {\n return p;\n }\n return createProxy(\n target[prop as keyof T] as object,\n cache,\n opts,\n proxyCache,\n wrapperCache,\n currentPath,\n );\n },\n });\n proxyCache[path] = proxy;\n return proxy;\n}\n\nexport function cacheProxy(\n api: T,\n opts: CacheProxyOpts = {},\n): T {\n const { cacheStore = new FileCacheStore() } = opts;\n return createProxy(api, cacheStore, opts, {}, {});\n}\n"], + "mappings": "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", + "names": ["NotFound", "SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "fs", "path", "MemCacheStore", "cache", "key", "expire", "entry", "NotFound", "value", "FileCacheStore", "MemCacheStore", "opts", "data", "key", "value", "entry", "createProxy", "api", "cache", "opts", "proxyCache", "wrapperCache", "path", "proxy", "target", "prop", "currentPath", "p", "binded", "defaultExpire", "DAY", "pathExpire", "onHit", "onMiss", "expire", "wrapped", "args", "key", "cached", "NotFound", "result", "cacheProxy", "cacheStore", "FileCacheStore"] +} diff --git a/package-lock.json b/package-lock.json index 82ee0af..91a2eb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amemo", - "version": "1.2.0", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amemo", - "version": "1.2.0", + "version": "1.2.3", "license": "MIT", "devDependencies": { "@babel/core": "^7.24.4", diff --git a/package.json b/package.json index 4c4149a..70589f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amemo", - "version": "1.2.0", + "version": "1.2.3", "description": "amemo is an experimental drop-in typesafe memoization library", "repository": "https://github.com/enda-automation/amemo", "main": "dist/index.js", @@ -14,7 +14,7 @@ "format": "prettier --write src/ tests/", "format:check": "prettier --check src/ tests/", "lint": "eslint src/ tests/", - "release": "standard-version" + "release": "standard-version && npm run build && npm run prepublish" }, "keywords": [], "author": "Engin Aydogan ", diff --git a/tests/cacheProxy.test.ts b/tests/cacheProxy.test.ts index f9205d7..6c52806 100644 --- a/tests/cacheProxy.test.ts +++ b/tests/cacheProxy.test.ts @@ -15,6 +15,8 @@ class NestedNestedTest { public bar(_ = "bar") { return this.barCalls++; } + + public readonly shouldNotBeCached = "shouldNotBeCached"; } class NestedTest { @@ -210,4 +212,16 @@ describe("cacheProxy", () => { expect(c.main()).toBe(0); expect(c.main()).toBe(0); }); + + it("must not cache non-functions", async () => { + const t = new Test(); + const c = cacheProxy(t); + const write = mockFs.writeFileSync.mockImplementation(() => {}); + expect(c.nested.nested.shouldNotBeCached).toBe("shouldNotBeCached"); + expect(c.nested.nested.shouldNotBeCached).toBe("shouldNotBeCached"); + expect(write).toBeCalledTimes(0); + expect(c.main()).toBe(0); + expect(c.main()).toBe(0); + expect(write).toBeCalledTimes(1); + }); });