@ooneex/cache 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -50,9 +50,11 @@ declare class FilesystemCache extends AbstractCache {
|
|
|
50
50
|
private readCacheEntry;
|
|
51
51
|
private writeCacheEntry;
|
|
52
52
|
}
|
|
53
|
+
import { AppEnv } from "@ooneex/app-env";
|
|
53
54
|
declare class RedisCache extends AbstractCache {
|
|
55
|
+
private readonly env;
|
|
54
56
|
private client;
|
|
55
|
-
constructor(options?: RedisCacheOptionsType);
|
|
57
|
+
constructor(env: AppEnv, options?: RedisCacheOptionsType);
|
|
56
58
|
protected connect(): Promise<void>;
|
|
57
59
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
58
60
|
set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var u=function(e,t,n,i){var s=arguments.length,o=s<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,n):i,
|
|
2
|
+
var u=function(e,t,n,i){var s=arguments.length,o=s<3?t:i===null?i=Object.getOwnPropertyDescriptor(t,n):i,a;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")o=Reflect.decorate(e,t,n,i);else for(var d=e.length-1;d>=0;d--)if(a=e[d])o=(s<3?a(o):s>3?a(t,n,o):a(t,n))||o;return s>3&&o&&Object.defineProperty(t,n,o),o},y=(e,t)=>(n,i)=>t(n,i,e),h=(e,t)=>{if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(e,t)};var g=import.meta.require;import{Exception as b}from"@ooneex/exception";import{HttpStatus as w}from"@ooneex/http-status";class r extends b{constructor(e,t={}){super(e,{status:w.Code.InternalServerError,data:t});this.name="CacheException"}}class c{async withConnection(e,t){try{return await this.connect(),await t()}catch(n){if(n instanceof r)throw n;throw new r(`${e}: ${n}`)}}}import{container as C,EContainerScope as T}from"@ooneex/container";var l={cache:(e=T.Singleton)=>{return(t)=>{C.add(t,e)}}};class p extends c{cacheDir;maxFileSize;constructor(e={}){super();this.cacheDir=e.cacheDir||`${process.cwd()}/.cache`,this.maxFileSize=e.maxFileSize||10485760}async connect(){try{let{mkdir:e,stat:t}=await import("fs/promises");if(await e(this.cacheDir,{recursive:!0}),!(await t(this.cacheDir)).isDirectory())throw new r("Failed to create cache directory")}catch(e){throw new r(`Failed to initialize filesystem cache: ${e}`)}}async get(e){return this.withConnection(`Failed to get key "${e}"`,async()=>{return(await this.readCacheEntry(e))?.value})}async set(e,t,n){return this.withConnection(`Failed to set key "${e}"`,async()=>{let i={value:t,createdAt:Date.now(),originalKey:e,...n!==void 0&&{ttl:n}};await this.writeCacheEntry(e,i)})}async delete(e){return this.withConnection(`Failed to delete key "${e}"`,async()=>{let t=Bun.file(this.getFilePath(e));if(!await t.exists())return!1;return await t.delete(),!0})}async has(e){return this.withConnection(`Failed to check if key "${e}" exists`,async()=>{return await this.readCacheEntry(e)!==void 0})}getFilePath(e){if(e.length>200){let n=Bun.hash(e);return`${this.cacheDir}/${n.toString(36)}.cache`}let t=e.replace(/[<>:"/\\|?*\x00-\x1f]/g,"_");return`${this.cacheDir}/${t}.cache`}isExpired(e){if(!e.ttl)return!1;if(e.ttl===0)return!1;return e.createdAt+e.ttl*1000<Date.now()}async readCacheEntry(e){try{let t=Bun.file(this.getFilePath(e));if(!await t.exists())return;let n=await t.text(),i=JSON.parse(n);if(this.isExpired(i)){await t.delete().catch(()=>{});return}return i}catch{return}}async writeCacheEntry(e,t){let n=JSON.stringify(t);if(Buffer.byteLength(n,"utf-8")>this.maxFileSize)throw new r(`Cache entry exceeds maximum file size of ${this.maxFileSize} bytes`);await Bun.write(this.getFilePath(e),n)}}p=u([l.cache(),h("design:paramtypes",[typeof FilesystemCacheOptionsType==="undefined"?Object:FilesystemCacheOptionsType])],p);import{AppEnv as f}from"@ooneex/app-env";import{inject as x}from"@ooneex/container";class m extends c{env;client;constructor(e,t={}){super();this.env=e;let n=t.connectionString||this.env.CACHE_REDIS_URL;if(!n)throw new r("Redis connection string is required. Please provide a connection string either through the constructor options or set the CACHE_REDIS_URL environment variable.");let{connectionString:i,...s}=t,a={...{connectionTimeout:1e4,idleTimeout:30000,autoReconnect:!0,maxRetries:3,enableOfflineQueue:!0,enableAutoPipelining:!0},...s};this.client=new Bun.RedisClient(n,a)}async connect(){if(!this.client.connected)await this.client.connect()}async get(e){return this.withConnection(`Failed to get key "${e}"`,async()=>{let t=await this.client.get(e);if(t===null)return;try{return JSON.parse(t)}catch{return t}})}async set(e,t,n){return this.withConnection(`Failed to set key "${e}"`,async()=>{let i=t===void 0?null:t,s=typeof i==="string"?i:JSON.stringify(i);if(await this.client.set(e,s),n&&n>0)await this.client.expire(e,n)})}async delete(e){return this.withConnection(`Failed to delete key "${e}"`,async()=>{return await this.client.del(e)>0})}async has(e){return this.withConnection(`Failed to check if key "${e}" exists`,async()=>{return await this.client.exists(e)})}}m=u([l.cache(),y(0,x(f)),h("design:paramtypes",[typeof f==="undefined"?Object:f,typeof RedisCacheOptionsType==="undefined"?Object:RedisCacheOptionsType])],m);export{l as decorator,m as RedisCache,p as FilesystemCache,r as CacheException,c as AbstractCache};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=B3051906C3F4E6BD64756E2164756E21
|
package/dist/index.js.map
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
"import { CacheException } from \"./CacheException\";\nimport type { ICache } from \"./types\";\n\nexport abstract class AbstractCache implements ICache {\n protected abstract connect(): Promise<void>;\n\n protected async withConnection<T>(errorMessage: string, fn: () => Promise<T>): Promise<T> {\n try {\n await this.connect();\n\n return await fn();\n } catch (error) {\n if (error instanceof CacheException) {\n throw error;\n }\n\n throw new CacheException(`${errorMessage}: ${error}`);\n }\n }\n\n public abstract get<T = unknown>(key: string): Promise<T | undefined>;\n public abstract set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;\n public abstract delete(key: string): Promise<boolean>;\n public abstract has(key: string): Promise<boolean>;\n}\n",
|
|
7
7
|
"import { container, EContainerScope } from \"@ooneex/container\";\nimport type { CacheClassType } from \"./types\";\n\nexport const decorator = {\n cache: (scope: EContainerScope = EContainerScope.Singleton) => {\n return (target: CacheClassType): void => {\n container.add(target, scope);\n };\n },\n};\n",
|
|
8
8
|
"import { AbstractCache } from \"./AbstractCache\";\nimport { CacheException } from \"./CacheException\";\nimport { decorator } from \"./decorators\";\nimport type { FilesystemCacheOptionsType } from \"./types\";\n\ntype CacheEntryType<T = unknown> = {\n value: T;\n ttl?: number;\n createdAt: number;\n originalKey: string;\n};\n\n@decorator.cache()\nexport class FilesystemCache extends AbstractCache {\n private cacheDir: string;\n private maxFileSize: number;\n\n constructor(options: FilesystemCacheOptionsType = {}) {\n super();\n this.cacheDir = options.cacheDir || `${process.cwd()}/.cache`;\n this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB default\n }\n\n protected async connect(): Promise<void> {\n try {\n const { mkdir, stat } = await import(\"node:fs/promises\");\n await mkdir(this.cacheDir, { recursive: true });\n\n const stats = await stat(this.cacheDir);\n if (!stats.isDirectory()) {\n throw new CacheException(\"Failed to create cache directory\");\n }\n } catch (error) {\n throw new CacheException(`Failed to initialize filesystem cache: ${error}`);\n }\n }\n\n public async get<T = unknown>(key: string): Promise<T | undefined> {\n return this.withConnection(`Failed to get key \"${key}\"`, async () => {\n const entry = await this.readCacheEntry<T>(key);\n\n return entry?.value;\n });\n }\n\n public async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n return this.withConnection(`Failed to set key \"${key}\"`, async () => {\n const entry: CacheEntryType<T> = {\n value,\n createdAt: Date.now(),\n originalKey: key,\n ...(ttl !== undefined && { ttl }),\n };\n\n await this.writeCacheEntry(key, entry);\n });\n }\n\n public async delete(key: string): Promise<boolean> {\n return this.withConnection(`Failed to delete key \"${key}\"`, async () => {\n const file = Bun.file(this.getFilePath(key));\n\n if (!(await file.exists())) {\n return false;\n }\n\n await file.delete();\n\n return true;\n });\n }\n\n public async has(key: string): Promise<boolean> {\n return this.withConnection(`Failed to check if key \"${key}\" exists`, async () => {\n const entry = await this.readCacheEntry(key);\n\n return entry !== undefined;\n });\n }\n\n private getFilePath(key: string): string {\n if (key.length > 200) {\n const hash = Bun.hash(key);\n return `${this.cacheDir}/${hash.toString(36)}.cache`;\n }\n\n const sanitizedKey = key.replace(/[<>:\"/\\\\|?*\\x00-\\x1f]/g, \"_\");\n\n return `${this.cacheDir}/${sanitizedKey}.cache`;\n }\n\n private isExpired(entry: CacheEntryType): boolean {\n if (!entry.ttl) return false;\n\n if (entry.ttl === 0) {\n return false;\n }\n\n return entry.createdAt + entry.ttl * 1000 < Date.now();\n }\n\n private async readCacheEntry<T>(key: string): Promise<CacheEntryType<T> | undefined> {\n try {\n const file = Bun.file(this.getFilePath(key));\n\n if (!(await file.exists())) {\n return;\n }\n\n const content = await file.text();\n const entry: CacheEntryType<T> = JSON.parse(content);\n\n if (this.isExpired(entry)) {\n await file.delete().catch(() => {});\n return;\n }\n\n return entry;\n } catch {\n return;\n }\n }\n\n private async writeCacheEntry<T>(key: string, entry: CacheEntryType<T>): Promise<void> {\n const content = JSON.stringify(entry);\n\n if (Buffer.byteLength(content, \"utf-8\") > this.maxFileSize) {\n throw new CacheException(`Cache entry exceeds maximum file size of ${this.maxFileSize} bytes`);\n }\n\n await Bun.write(this.getFilePath(key), content);\n }\n}\n",
|
|
9
|
-
"import { AbstractCache } from \"./AbstractCache\";\nimport { CacheException } from \"./CacheException\";\nimport { decorator } from \"./decorators\";\nimport type { RedisCacheOptionsType } from \"./types\";\n\n@decorator.cache()\nexport class RedisCache extends AbstractCache {\n private client: Bun.RedisClient;\n\n constructor(options: RedisCacheOptionsType = {}) {\n super();\n const connectionString = options.connectionString ||
|
|
9
|
+
"import { AppEnv } from \"@ooneex/app-env\";\nimport { inject } from \"@ooneex/container\";\nimport { AbstractCache } from \"./AbstractCache\";\nimport { CacheException } from \"./CacheException\";\nimport { decorator } from \"./decorators\";\nimport type { RedisCacheOptionsType } from \"./types\";\n\n@decorator.cache()\nexport class RedisCache extends AbstractCache {\n private client: Bun.RedisClient;\n\n constructor(\n @inject(AppEnv) private readonly env: AppEnv,\n options: RedisCacheOptionsType = {},\n ) {\n super();\n const connectionString = options.connectionString || this.env.CACHE_REDIS_URL;\n\n if (!connectionString) {\n throw new CacheException(\n \"Redis connection string is required. Please provide a connection string either through the constructor options or set the CACHE_REDIS_URL environment variable.\",\n );\n }\n\n const { connectionString: _, ...userOptions } = options;\n\n const defaultOptions = {\n connectionTimeout: 10_000,\n idleTimeout: 30_000,\n autoReconnect: true,\n maxRetries: 3,\n enableOfflineQueue: true,\n enableAutoPipelining: true,\n };\n\n const clientOptions = { ...defaultOptions, ...userOptions };\n\n this.client = new Bun.RedisClient(connectionString, clientOptions);\n }\n\n protected async connect(): Promise<void> {\n if (!this.client.connected) {\n await this.client.connect();\n }\n }\n\n public async get<T = unknown>(key: string): Promise<T | undefined> {\n return this.withConnection(`Failed to get key \"${key}\"`, async () => {\n const value = await this.client.get(key);\n\n if (value === null) {\n return;\n }\n\n try {\n return JSON.parse(value);\n } catch {\n return value as T;\n }\n });\n }\n\n public async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n return this.withConnection(`Failed to set key \"${key}\"`, async () => {\n const normalizedValue = value === undefined ? null : value;\n const serializedValue = typeof normalizedValue === \"string\" ? normalizedValue : JSON.stringify(normalizedValue);\n\n await this.client.set(key, serializedValue);\n\n if (ttl && ttl > 0) {\n await this.client.expire(key, ttl);\n }\n });\n }\n\n public async delete(key: string): Promise<boolean> {\n return this.withConnection(`Failed to delete key \"${key}\"`, async () => {\n const result = await this.client.del(key);\n\n return result > 0;\n });\n }\n\n public async has(key: string): Promise<boolean> {\n return this.withConnection(`Failed to check if key \"${key}\" exists`, async () => {\n const result = await this.client.exists(key);\n\n return result;\n });\n }\n}\n"
|
|
10
10
|
],
|
|
11
|
-
"mappings": ";
|
|
12
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "B3051906C3F4E6BD64756E2164756E21",
|
|
13
13
|
"names": []
|
|
14
14
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ooneex/cache",
|
|
3
3
|
"description": "High-performance caching layer with filesystem and Redis backends — features TTL expiration, auto-serialization, configurable size limits, and dependency injection integration",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"test": "bun test tests",
|
|
26
26
|
"build": "bunup",
|
|
27
27
|
"lint": "tsgo --noEmit && bunx biome lint",
|
|
28
|
-
"npm:publish": "bun publish --tolerate-republish --access public"
|
|
28
|
+
"npm:publish": "bun publish --tolerate-republish --force --production --access public"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@ooneex/
|
|
32
|
-
"@ooneex/
|
|
33
|
-
"@ooneex/
|
|
31
|
+
"@ooneex/app-env": "1.0.2",
|
|
32
|
+
"@ooneex/container": "1.0.1",
|
|
33
|
+
"@ooneex/exception": "1.0.1",
|
|
34
|
+
"@ooneex/http-status": "1.0.1"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {},
|
|
36
37
|
"keywords": [
|