klaim 1.3.0 → 1.4.0

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/README.md CHANGED
@@ -15,6 +15,7 @@ experience.
15
15
  - [Request Handling](#request-handling)
16
16
  - [Middleware Usage](#middleware-usage)
17
17
  - [Hook Subscription](#hook-subscription)
18
+ - [Caching Requests](#caching-requests)
18
19
  - [Links](#-links)
19
20
  - [Contributing](#-contributing)
20
21
  - [License](#-license)
@@ -27,6 +28,7 @@ experience.
27
28
  - **Lightweight**: Minimal footprint for fast load times and minimal performance impact.
28
29
  - **Middleware Support**: Easily add middleware to modify requests and responses (`before` and `after`).
29
30
  - **Hook System**: Subscribe to hooks to monitor and react to specific events.
31
+ - **Caching**: Enable caching on requests to reduce network load and improve performance.
30
32
  - **TypeScript Support**: Fully typed for enhanced code quality and developer experience.
31
33
 
32
34
  ## 📥 Installation
@@ -140,6 +142,43 @@ Hook.subscribe("hello.getFirstTodo", ({url}) => {
140
142
  });
141
143
  ```
142
144
 
145
+ ### Caching Requests
146
+
147
+ Enable caching on requests to reduce network load and improve performance. By default, the cache duration is 20 seconds,
148
+ but you can specify a custom duration in seconds.
149
+
150
+ #### Caching Individual Routes
151
+
152
+ You can enable caching on individual routes:
153
+
154
+ ```typescript
155
+ Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
156
+ // Get a list of todos with default cache duration (20 seconds)
157
+ Route.get<Todo[]>("listTodos", "todos").withCache();
158
+
159
+ // Get a specific todo by id with custom cache duration (300 seconds)
160
+ Route.get<Todo>("getTodo", "todos/[id]").withCache(300);
161
+
162
+ // Add a new todo (no cache)
163
+ Route.post<Todo>("addTodo", "todos");
164
+ });
165
+ ```
166
+
167
+ Now, when making requests, the caching feature will be applied.
168
+
169
+ #### Caching the Entire API
170
+
171
+ You can also enable caching for all routes defined within an API:
172
+
173
+ ```typescript
174
+ Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
175
+ // Define routes for the API
176
+ Route.get<Todo[]>("listTodos", "todos");
177
+ Route.get<Todo>("getTodo", "todos/[id]");
178
+ Route.post<Todo>("addTodo", "todos");
179
+ }).withCache(); // Enable default cache duration (20 seconds) for all routes
180
+ ```
181
+
143
182
  ## 🔗 Links
144
183
 
145
184
  - [NPM](https://www.npmjs.com/package/klaim)
package/bun.lockb CHANGED
File without changes
package/deno.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "@antharuu/klaim",
3
- "version": "1.3.0",
4
- "description": "Klaim is a lightweight TypeScript library designed to manage APIs and record requests, optimized for an optimal user experience.",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/antharuu/klaim.git"
8
- },
9
- "keywords": [
10
- "typescript",
11
- "api",
12
- "request",
13
- "user experience",
14
- "optimization",
15
- "lightweight"
16
- ],
17
- "author": "antharuu",
18
- "license": "MIT",
19
- "bugs": {
20
- "url": "https://github.com/antharuu/klaim/issues"
21
- },
22
- "homepage": "https://github.com/antharuu/klaim#readme",
23
- "main": "dist/klaim.cjs.js",
24
- "module": "dist/klaim.es.js",
25
- "types": "dist/index.d.ts",
26
- "exports": "./mod.ts",
27
- "scripts": {
28
- "build": "vite build",
29
- "dev": "vite build --watch",
30
- "test": "vitest",
31
- "lint": "eslint src",
32
- "lint:fix": "eslint src **/*.ts --fix",
33
- "release": "dotenv release-it --"
34
- },
35
- "peerDependencies": {
36
- "typescript": "^5.5.3"
37
- }
38
- }
1
+ {
2
+ "name": "@antharuu/klaim",
3
+ "version": "1.4.0",
4
+ "description": "Klaim is a lightweight TypeScript library designed to manage APIs and record requests, optimized for an optimal user experience.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/antharuu/klaim.git"
8
+ },
9
+ "keywords": [
10
+ "typescript",
11
+ "api",
12
+ "request",
13
+ "user experience",
14
+ "optimization",
15
+ "lightweight"
16
+ ],
17
+ "author": "antharuu",
18
+ "license": "MIT",
19
+ "bugs": {
20
+ "url": "https://github.com/antharuu/klaim/issues"
21
+ },
22
+ "homepage": "https://github.com/antharuu/klaim#readme",
23
+ "main": "dist/klaim.cjs.js",
24
+ "module": "dist/klaim.es.js",
25
+ "types": "dist/index.d.ts",
26
+ "exports": "./mod.ts",
27
+ "scripts": {
28
+ "build": "vite build",
29
+ "dev": "vite build --watch",
30
+ "test": "vitest",
31
+ "lint": "eslint src",
32
+ "lint:fix": "eslint src **/*.ts --fix",
33
+ "release": "dotenv release-it --"
34
+ },
35
+ "peerDependencies": {
36
+ "typescript": "^5.5.3"
37
+ }
38
+ }
package/dist/klaim.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var $=Object.defineProperty;var k=(a,t,e)=>t in a?$(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var i=(a,t,e)=>k(a,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function d(a){return a.trim().replace(/^\/|\/$/g,"")}function A(a){return a.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}class p{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}i(p,"_callbacks",new Map);var g=(a=>(a.GET="GET",a.POST="POST",a.PUT="PUT",a.DELETE="DELETE",a.PATCH="PATCH",a.OPTIONS="OPTIONS",a))(g||{});class h{constructor(t,e,n,r="GET"){i(this,"api","undefined");i(this,"name");i(this,"url");i(this,"method");i(this,"headers");i(this,"arguments",new Set);i(this,"callbacks",{before:null,after:null});this.name=A(t),this.name!==t&&console.warn(`Route name "${t}" has been camelCased to "${this.name}"`),this.url=d(e),this.headers=n||{},this.method=r,this.detectArguments()}static createRoute(t,e,n,r){const s=new h(t,e,n,r);return o.i.registerRoute(s),s}static get(t,e,n={}){return this.createRoute(t,e,n,"GET")}static post(t,e,n){return this.createRoute(t,e,n,"POST")}static put(t,e,n){return this.createRoute(t,e,n,"PUT")}static delete(t,e,n){return this.createRoute(t,e,n,"DELETE")}static patch(t,e,n){return this.createRoute(t,e,n,"PATCH")}static options(t,e,n){return this.createRoute(t,e,n,"OPTIONS")}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const n=e.replace(/\[|]/g,"");this.arguments.add(n)})}}const l={};async function O(a,t,e={},n={}){let r=_(`${a.url}/${t.url}`,t,e),s={};n&&t.method!==g.GET&&(s.body=JSON.stringify(n)),s.headers={"Content-Type":"application/json",...a.headers,...t.headers},s.method=t.method;const{beforeRoute:u,beforeApi:w,beforeUrl:T,beforeConfig:b}=y({route:t,api:a,url:r,config:s});r=T,s=b,a=o.updateApi(w),t=o.updateRoute(u);const m=await fetch(r,s),{afterRoute:E,afterApi:P,afterData:C}=S({route:t,api:a,response:m,data:await m.json()});return o.updateApi(P),o.updateRoute(E),p.run(`${a.name}.${t.name}`),C}function _(a,t,e){let n=a;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);n=n.replace(`[${r}]`,e[r])}),n}function y({route:a,api:t,url:e,config:n}){var s,u;const r=(u=(s=a.callbacks).before)==null?void 0:u.call(s,{route:a,api:t,url:e,config:n});return{beforeRoute:(r==null?void 0:r.route)||a,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||n}}function S({route:a,api:t,response:e,data:n}){var s,u;const r=(u=(s=a.callbacks).after)==null?void 0:u.call(s,{route:a,api:t,response:e,data:n});return{afterRoute:(r==null?void 0:r.route)||a,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||n}}const c=class c{constructor(){i(this,"_apis",new Map);i(this,"_currentApi",null)}static get i(){return c._instance||(c._instance=new c),c._instance}registerApi(t){this._apis.set(t.name,t),l[t.name]={}}setCurrent(t){const e=this._apis.get(t);if(!e)throw new Error(`API ${t} not found`);this._currentApi=e}clearCurrent(){this._currentApi=null}registerRoute(t){if(!this._currentApi)throw new Error("No current API set, use Route only inside Api.create callback");t.api=this._currentApi.name,this._currentApi.routes.set(t.name,t),this.addToKlaimRoute(t.api,t)}getApi(t){return this._apis.get(t)}getRoute(t,e){const n=this._apis.get(t);if(!n)throw new Error(`API ${t} not found`);return n.routes.get(e)}static updateApi(t){return c.i._apis.has(t.name)||c.i.registerApi(t),c.i._apis.set(t.name,t),t}static updateRoute(t){const e=c.i._apis.get(t.api);if(!e)throw new Error(`API ${t.api} not found`);return e.routes.set(t.name,t),t}addToKlaimRoute(t,e){l[t][e.name]=async(n={},r={})=>{const s=c.i._apis.get(t);if(!s)throw new Error(`API ${e.api} not found`);return O(s,e,n,r)}}};i(c,"_instance");let o=c;class f{constructor(t,e,n={}){i(this,"name");i(this,"url");i(this,"headers");i(this,"routes",new Map);this.name=t,this.url=d(e),this.headers=n}static create(t,e,n,r={}){const s=A(t);s!==t&&console.warn(`API name "${t}" has been camelCased to "${s}"`);const u=new f(s,e,r);return o.i.registerApi(u),o.i.setCurrent(s),n(),o.i.clearCurrent(),u}}exports.Api=f;exports.Hook=p;exports.Klaim=l;exports.Registry=o;exports.Route=h;
1
+ "use strict";var O=Object.defineProperty;var _=(n,t,e)=>t in n?O(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var i=(n,t,e)=>_(n,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function m(n){return n.trim().replace(/^\/|\/$/g,"")}function A(n){return n.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}const u=class u{constructor(){i(this,"cache");this.cache=new Map}static get i(){return u._instance||(u._instance=new u),u._instance}set(t,e,r){const a=Date.now()+r;this.cache.set(t,{data:e,expiry:a})}has(t){const e=this.cache.get(t);return e?Date.now()>e.expiry?(this.cache.delete(t),!1):!0:!1}get(t){return this.has(t)?this.cache.get(t).data:null}};i(u,"_instance");let l=u;function k(n){let r=2166136261;for(let s=0;s<n.length;s++)r^=n.charCodeAt(s),r*=16777619;let a=(r>>>0).toString(16).padStart(8,"0");for(;a.length<32;)r^=a.charCodeAt(a.length%a.length),r*=16777619,a+=(r>>>0).toString(16).padStart(8,"0");return a.substring(0,32)}async function I(n,t,e){const r=`${n.toString()}${JSON.stringify(t)}`,a=k(r);if(l.i.has(a))return l.i.get(a);const c=await(await fetch(n,t)).json();return l.i.set(a,c,e),c}class g{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}i(g,"_callbacks",new Map);var b=(n=>(n.GET="GET",n.POST="POST",n.PUT="PUT",n.DELETE="DELETE",n.PATCH="PATCH",n.OPTIONS="OPTIONS",n))(b||{});class d{constructor(t,e,r,a="GET"){i(this,"api","undefined");i(this,"name");i(this,"url");i(this,"method");i(this,"headers");i(this,"arguments",new Set);i(this,"callbacks",{before:null,after:null});this.name=A(t),this.name!==t&&console.warn(`Route name "${t}" has been camelCased to "${this.name}"`),this.url=m(e),this.headers=r||{},this.method=a,this.detectArguments()}static createRoute(t,e,r,a){const s=new d(t,e,r,a);return h.i.registerRoute(s),s}static get(t,e,r={}){return this.createRoute(t,e,r,"GET")}static post(t,e,r){return this.createRoute(t,e,r,"POST")}static put(t,e,r){return this.createRoute(t,e,r,"PUT")}static delete(t,e,r){return this.createRoute(t,e,r,"DELETE")}static patch(t,e,r){return this.createRoute(t,e,r,"PATCH")}static options(t,e,r){return this.createRoute(t,e,r,"OPTIONS")}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const r=e.replace(/\[|]/g,"");this.arguments.add(r)})}withCache(t=20){return this.cache=t,this}}const f={};async function U(n,t,e={},r={}){let a=D(`${n.url}/${t.url}`,t,e),s={};r&&t.method!==b.GET&&(s.body=JSON.stringify(r)),s.headers={"Content-Type":"application/json",...n.headers,...t.headers},s.method=t.method;const{beforeRoute:c,beforeApi:T,beforeUrl:E,beforeConfig:C}=H({route:t,api:n,url:a,config:s});a=E,s=C,n=h.updateApi(T),t=h.updateRoute(c);const P=n.cache||t.cache;let p;P?p=await I(a,s,n.cache):p=await(await fetch(a,s)).json();const{afterRoute:S,afterApi:$,afterData:y}=N({route:t,api:n,response:p,data:p});return h.updateApi($),h.updateRoute(S),g.run(`${n.name}.${t.name}`),y}function D(n,t,e){let r=n;return t.arguments.forEach(a=>{if(e[a]===void 0)throw new Error(`Argument ${a} is missing`);r=r.replace(`[${a}]`,e[a])}),r}function H({route:n,api:t,url:e,config:r}){var s,c;const a=(c=(s=n.callbacks).before)==null?void 0:c.call(s,{route:n,api:t,url:e,config:r});return{beforeRoute:(a==null?void 0:a.route)||n,beforeApi:(a==null?void 0:a.api)||t,beforeUrl:(a==null?void 0:a.url)||e,beforeConfig:(a==null?void 0:a.config)||r}}function N({route:n,api:t,response:e,data:r}){var s,c;const a=(c=(s=n.callbacks).after)==null?void 0:c.call(s,{route:n,api:t,response:e,data:r});return{afterRoute:(a==null?void 0:a.route)||n,afterApi:(a==null?void 0:a.api)||t,afterResponse:(a==null?void 0:a.response)||e,afterData:(a==null?void 0:a.data)||r}}const o=class o{constructor(){i(this,"_apis",new Map);i(this,"_currentApi",null)}static get i(){return o._instance||(o._instance=new o),o._instance}registerApi(t){this._apis.set(t.name,t),f[t.name]={}}setCurrent(t){const e=this._apis.get(t);if(!e)throw new Error(`API ${t} not found`);this._currentApi=e}clearCurrent(){this._currentApi=null}registerRoute(t){if(!this._currentApi)throw new Error("No current API set, use Route only inside Api.create callback");t.api=this._currentApi.name,this._currentApi.routes.set(t.name,t),this.addToKlaimRoute(t.api,t)}getApi(t){return this._apis.get(t)}getRoute(t,e){const r=this._apis.get(t);if(!r)throw new Error(`API ${t} not found`);return r.routes.get(e)}static updateApi(t){return o.i._apis.has(t.name)||o.i.registerApi(t),o.i._apis.set(t.name,t),t}static updateRoute(t){const e=o.i._apis.get(t.api);if(!e)throw new Error(`API ${t.api} not found`);return e.routes.set(t.name,t),t}addToKlaimRoute(t,e){f[t][e.name]=async(r={},a={})=>{const s=o.i._apis.get(t);if(!s)throw new Error(`API ${e.api} not found`);return U(s,e,r,a)}}};i(o,"_instance");let h=o;class w{constructor(t,e,r={}){i(this,"name");i(this,"url");i(this,"headers");i(this,"routes",new Map);i(this,"cache",!1);this.name=t,this.url=m(e),this.headers=r}static create(t,e,r,a={}){const s=A(t);s!==t&&console.warn(`API name "${t}" has been camelCased to "${s}"`);const c=new w(s,e,a);return h.i.registerApi(c),h.i.setCurrent(s),r(),h.i.clearCurrent(),c}withCache(t=20){return this.cache=t,this}}exports.Api=w;exports.Hook=g;exports.Klaim=f;exports.Registry=h;exports.Route=d;
package/dist/klaim.es.js CHANGED
@@ -1,13 +1,78 @@
1
- var $ = Object.defineProperty;
2
- var _ = (a, t, e) => t in a ? $(a, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[t] = e;
3
- var i = (a, t, e) => _(a, typeof t != "symbol" ? t + "" : t, e);
4
- function h(a) {
5
- return a.trim().replace(/^\/|\/$/g, "");
1
+ var y = Object.defineProperty;
2
+ var _ = (r, t, e) => t in r ? y(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
3
+ var i = (r, t, e) => _(r, typeof t != "symbol" ? t + "" : t, e);
4
+ function d(r) {
5
+ return r.trim().replace(/^\/|\/$/g, "");
6
6
  }
7
- function f(a) {
8
- return a.replace(/([-_][a-z])/gi, (t) => t.toUpperCase().replace("-", "").replace("_", "")).replace(/(^\w)/, (t) => t.toLowerCase());
7
+ function g(r) {
8
+ return r.replace(/([-_][a-z])/gi, (t) => t.toUpperCase().replace("-", "").replace("_", "")).replace(/(^\w)/, (t) => t.toLowerCase());
9
9
  }
10
- class m {
10
+ const u = class u {
11
+ /**
12
+ * Private constructor to prevent instantiation from outside the class.
13
+ */
14
+ constructor() {
15
+ i(this, "cache");
16
+ this.cache = /* @__PURE__ */ new Map();
17
+ }
18
+ /**
19
+ * Provides access to the singleton instance of the class.
20
+ *
21
+ * @returns The singleton instance of Cache.
22
+ */
23
+ static get i() {
24
+ return u._instance || (u._instance = new u()), u._instance;
25
+ }
26
+ /**
27
+ * Sets the cache for a specific key.
28
+ *
29
+ * @param key The key to cache the value under.
30
+ * @param value The value to be cached.
31
+ * @param ttl Time to live in milliseconds.
32
+ */
33
+ set(t, e, n) {
34
+ const a = Date.now() + n;
35
+ this.cache.set(t, { data: e, expiry: a });
36
+ }
37
+ /**
38
+ * Checks if the cache has a valid entry for the given key.
39
+ *
40
+ * @param key The key to check in the cache.
41
+ * @returns True if a valid cache entry exists, otherwise false.
42
+ */
43
+ has(t) {
44
+ const e = this.cache.get(t);
45
+ return e ? Date.now() > e.expiry ? (this.cache.delete(t), !1) : !0 : !1;
46
+ }
47
+ /**
48
+ * Gets the cached value for the given key.
49
+ *
50
+ * @param key The key to retrieve from the cache.
51
+ * @returns The cached value or null if not found or expired.
52
+ */
53
+ get(t) {
54
+ return this.has(t) ? this.cache.get(t).data : null;
55
+ }
56
+ };
57
+ i(u, "_instance");
58
+ let l = u;
59
+ function k(r) {
60
+ let n = 2166136261;
61
+ for (let s = 0; s < r.length; s++)
62
+ n ^= r.charCodeAt(s), n *= 16777619;
63
+ let a = (n >>> 0).toString(16).padStart(8, "0");
64
+ for (; a.length < 32; )
65
+ n ^= a.charCodeAt(a.length % a.length), n *= 16777619, a += (n >>> 0).toString(16).padStart(8, "0");
66
+ return a.substring(0, 32);
67
+ }
68
+ async function I(r, t, e) {
69
+ const n = `${r.toString()}${JSON.stringify(t)}`, a = k(n);
70
+ if (l.i.has(a))
71
+ return l.i.get(a);
72
+ const c = await (await fetch(r, t)).json();
73
+ return l.i.set(a, c, e), c;
74
+ }
75
+ class w {
11
76
  /**
12
77
  * Subscribes to the hook
13
78
  *
@@ -27,8 +92,8 @@ class m {
27
92
  e && e();
28
93
  }
29
94
  }
30
- i(m, "_callbacks", /* @__PURE__ */ new Map());
31
- var d = /* @__PURE__ */ ((a) => (a.GET = "GET", a.POST = "POST", a.PUT = "PUT", a.DELETE = "DELETE", a.PATCH = "PATCH", a.OPTIONS = "OPTIONS", a))(d || {});
95
+ i(w, "_callbacks", /* @__PURE__ */ new Map());
96
+ var m = /* @__PURE__ */ ((r) => (r.GET = "GET", r.POST = "POST", r.PUT = "PUT", r.DELETE = "DELETE", r.PATCH = "PATCH", r.OPTIONS = "OPTIONS", r))(m || {});
32
97
  class A {
33
98
  /**
34
99
  * Constructor
@@ -38,7 +103,7 @@ class A {
38
103
  * @param headers - The headers to be sent with the request
39
104
  * @param method - The HTTP method of the route
40
105
  */
41
- constructor(t, e, n, r = "GET") {
106
+ constructor(t, e, n, a = "GET") {
42
107
  i(this, "api", "undefined");
43
108
  i(this, "name");
44
109
  i(this, "url");
@@ -55,7 +120,7 @@ class A {
55
120
  */
56
121
  after: null
57
122
  });
58
- this.name = f(t), this.name !== t && console.warn(`Route name "${t}" has been camelCased to "${this.name}"`), this.url = h(e), this.headers = n || {}, this.method = r, this.detectArguments();
123
+ this.name = g(t), this.name !== t && console.warn(`Route name "${t}" has been camelCased to "${this.name}"`), this.url = d(e), this.headers = n || {}, this.method = a, this.detectArguments();
59
124
  }
60
125
  /**
61
126
  * Creates a new route
@@ -66,9 +131,9 @@ class A {
66
131
  * @param method - The HTTP method of the route
67
132
  * @returns The new route
68
133
  */
69
- static createRoute(t, e, n, r) {
70
- const s = new A(t, e, n, r);
71
- return u.i.registerRoute(s), s;
134
+ static createRoute(t, e, n, a) {
135
+ const s = new A(t, e, n, a);
136
+ return h.i.registerRoute(s), s;
72
137
  }
73
138
  /**
74
139
  * Creates a new route with the GET method
@@ -200,58 +265,70 @@ class A {
200
265
  this.arguments.add(n);
201
266
  });
202
267
  }
268
+ /**
269
+ * Enables caching for the Route
270
+ *
271
+ * @param duration - The duration to cache the response for seconds (default: 20)
272
+ * @returns The Route
273
+ */
274
+ withCache(t = 20) {
275
+ return this.cache = t, this;
276
+ }
203
277
  }
204
- const p = {};
205
- async function k(a, t, e = {}, n = {}) {
206
- let r = O(`${a.url}/${t.url}`, t, e), s = {};
207
- n && t.method !== d.GET && (s.body = JSON.stringify(n)), s.headers = {
278
+ const f = {};
279
+ async function U(r, t, e = {}, n = {}) {
280
+ let a = D(`${r.url}/${t.url}`, t, e), s = {};
281
+ n && t.method !== m.GET && (s.body = JSON.stringify(n)), s.headers = {
208
282
  "Content-Type": "application/json",
209
- ...a.headers,
283
+ ...r.headers,
210
284
  ...t.headers
211
285
  }, s.method = t.method;
212
286
  const {
213
- beforeRoute: o,
214
- beforeApi: g,
215
- beforeUrl: T,
216
- beforeConfig: E
217
- } = I({ route: t, api: a, url: r, config: s });
218
- r = T, s = E, a = u.updateApi(g), t = u.updateRoute(o);
219
- const l = await fetch(r, s), {
220
- afterRoute: b,
221
- afterApi: P,
222
- afterData: C
223
- } = S({ route: t, api: a, response: l, data: await l.json() });
224
- return u.updateApi(P), u.updateRoute(b), m.run(`${a.name}.${t.name}`), C;
287
+ beforeRoute: c,
288
+ beforeApi: T,
289
+ beforeUrl: b,
290
+ beforeConfig: C
291
+ } = H({ route: t, api: r, url: a, config: s });
292
+ a = b, s = C, r = h.updateApi(T), t = h.updateRoute(c);
293
+ const P = r.cache || t.cache;
294
+ let p;
295
+ P ? p = await I(a, s, r.cache) : p = await (await fetch(a, s)).json();
296
+ const {
297
+ afterRoute: $,
298
+ afterApi: S,
299
+ afterData: O
300
+ } = N({ route: t, api: r, response: p, data: p });
301
+ return h.updateApi(S), h.updateRoute($), w.run(`${r.name}.${t.name}`), O;
225
302
  }
226
- function O(a, t, e) {
227
- let n = a;
228
- return t.arguments.forEach((r) => {
229
- if (e[r] === void 0)
230
- throw new Error(`Argument ${r} is missing`);
231
- n = n.replace(`[${r}]`, e[r]);
303
+ function D(r, t, e) {
304
+ let n = r;
305
+ return t.arguments.forEach((a) => {
306
+ if (e[a] === void 0)
307
+ throw new Error(`Argument ${a} is missing`);
308
+ n = n.replace(`[${a}]`, e[a]);
232
309
  }), n;
233
310
  }
234
- function I({ route: a, api: t, url: e, config: n }) {
235
- var s, o;
236
- const r = (o = (s = a.callbacks).before) == null ? void 0 : o.call(s, { route: a, api: t, url: e, config: n });
311
+ function H({ route: r, api: t, url: e, config: n }) {
312
+ var s, c;
313
+ const a = (c = (s = r.callbacks).before) == null ? void 0 : c.call(s, { route: r, api: t, url: e, config: n });
237
314
  return {
238
- beforeRoute: (r == null ? void 0 : r.route) || a,
239
- beforeApi: (r == null ? void 0 : r.api) || t,
240
- beforeUrl: (r == null ? void 0 : r.url) || e,
241
- beforeConfig: (r == null ? void 0 : r.config) || n
315
+ beforeRoute: (a == null ? void 0 : a.route) || r,
316
+ beforeApi: (a == null ? void 0 : a.api) || t,
317
+ beforeUrl: (a == null ? void 0 : a.url) || e,
318
+ beforeConfig: (a == null ? void 0 : a.config) || n
242
319
  };
243
320
  }
244
- function S({ route: a, api: t, response: e, data: n }) {
245
- var s, o;
246
- const r = (o = (s = a.callbacks).after) == null ? void 0 : o.call(s, { route: a, api: t, response: e, data: n });
321
+ function N({ route: r, api: t, response: e, data: n }) {
322
+ var s, c;
323
+ const a = (c = (s = r.callbacks).after) == null ? void 0 : c.call(s, { route: r, api: t, response: e, data: n });
247
324
  return {
248
- afterRoute: (r == null ? void 0 : r.route) || a,
249
- afterApi: (r == null ? void 0 : r.api) || t,
250
- afterResponse: (r == null ? void 0 : r.response) || e,
251
- afterData: (r == null ? void 0 : r.data) || n
325
+ afterRoute: (a == null ? void 0 : a.route) || r,
326
+ afterApi: (a == null ? void 0 : a.api) || t,
327
+ afterResponse: (a == null ? void 0 : a.response) || e,
328
+ afterData: (a == null ? void 0 : a.data) || n
252
329
  };
253
330
  }
254
- const c = class c {
331
+ const o = class o {
255
332
  /**
256
333
  * Constructor
257
334
  */
@@ -265,7 +342,7 @@ const c = class c {
265
342
  * @returns The singleton instance
266
343
  */
267
344
  static get i() {
268
- return c._instance || (c._instance = new c()), c._instance;
345
+ return o._instance || (o._instance = new o()), o._instance;
269
346
  }
270
347
  /**
271
348
  * Registers an API
@@ -273,7 +350,7 @@ const c = class c {
273
350
  * @param api - The API to register
274
351
  */
275
352
  registerApi(t) {
276
- this._apis.set(t.name, t), p[t.name] = {};
353
+ this._apis.set(t.name, t), f[t.name] = {};
277
354
  }
278
355
  /**
279
356
  * Sets the current API
@@ -331,7 +408,7 @@ const c = class c {
331
408
  * @returns The updated API
332
409
  */
333
410
  static updateApi(t) {
334
- return c.i._apis.has(t.name) || c.i.registerApi(t), c.i._apis.set(t.name, t), t;
411
+ return o.i._apis.has(t.name) || o.i.registerApi(t), o.i._apis.set(t.name, t), t;
335
412
  }
336
413
  /**
337
414
  * Updates a route
@@ -340,7 +417,7 @@ const c = class c {
340
417
  * @returns The updated route
341
418
  */
342
419
  static updateRoute(t) {
343
- const e = c.i._apis.get(t.api);
420
+ const e = o.i._apis.get(t.api);
344
421
  if (!e)
345
422
  throw new Error(`API ${t.api} not found`);
346
423
  return e.routes.set(t.name, t), t;
@@ -352,17 +429,17 @@ const c = class c {
352
429
  * @param route - The route to add
353
430
  */
354
431
  addToKlaimRoute(t, e) {
355
- p[t][e.name] = async (n = {}, r = {}) => {
356
- const s = c.i._apis.get(t);
432
+ f[t][e.name] = async (n = {}, a = {}) => {
433
+ const s = o.i._apis.get(t);
357
434
  if (!s)
358
435
  throw new Error(`API ${e.api} not found`);
359
- return k(s, e, n, r);
436
+ return U(s, e, n, a);
360
437
  };
361
438
  }
362
439
  };
363
- i(c, "_instance");
364
- let u = c;
365
- class w {
440
+ i(o, "_instance");
441
+ let h = o;
442
+ class E {
366
443
  /**
367
444
  * Constructor
368
445
  *
@@ -375,7 +452,8 @@ class w {
375
452
  i(this, "url");
376
453
  i(this, "headers");
377
454
  i(this, "routes", /* @__PURE__ */ new Map());
378
- this.name = t, this.url = h(e), this.headers = n;
455
+ i(this, "cache", !1);
456
+ this.name = t, this.url = d(e), this.headers = n;
379
457
  }
380
458
  /**
381
459
  * Creates a new API
@@ -386,17 +464,26 @@ class w {
386
464
  * @param headers - The headers to be sent with each request
387
465
  * @returns The new API
388
466
  */
389
- static create(t, e, n, r = {}) {
390
- const s = f(t);
467
+ static create(t, e, n, a = {}) {
468
+ const s = g(t);
391
469
  s !== t && console.warn(`API name "${t}" has been camelCased to "${s}"`);
392
- const o = new w(s, e, r);
393
- return u.i.registerApi(o), u.i.setCurrent(s), n(), u.i.clearCurrent(), o;
470
+ const c = new E(s, e, a);
471
+ return h.i.registerApi(c), h.i.setCurrent(s), n(), h.i.clearCurrent(), c;
472
+ }
473
+ /**
474
+ * Enables caching for the API
475
+ *
476
+ * @param duration - The duration to cache the response for seconds (default: 20)
477
+ * @returns The API
478
+ */
479
+ withCache(t = 20) {
480
+ return this.cache = t, this;
394
481
  }
395
482
  }
396
483
  export {
397
- w as Api,
398
- m as Hook,
399
- p as Klaim,
400
- u as Registry,
484
+ E as Api,
485
+ w as Hook,
486
+ f as Klaim,
487
+ h as Registry,
401
488
  A as Route
402
489
  };
package/dist/klaim.umd.js CHANGED
@@ -1 +1 @@
1
- (function(s,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(s=typeof globalThis<"u"?globalThis:s||self,o(s.klaim={}))})(this,function(s){"use strict";var S=Object.defineProperty;var I=(s,o,h)=>o in s?S(s,o,{enumerable:!0,configurable:!0,writable:!0,value:h}):s[o]=h;var c=(s,o,h)=>I(s,typeof o!="symbol"?o+"":o,h);function o(i){return i.trim().replace(/^\/|\/$/g,"")}function h(i){return i.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}class f{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}c(f,"_callbacks",new Map);var g=(i=>(i.GET="GET",i.POST="POST",i.PUT="PUT",i.DELETE="DELETE",i.PATCH="PATCH",i.OPTIONS="OPTIONS",i))(g||{});class d{constructor(t,e,r,n="GET"){c(this,"api","undefined");c(this,"name");c(this,"url");c(this,"method");c(this,"headers");c(this,"arguments",new Set);c(this,"callbacks",{before:null,after:null});this.name=h(t),this.name!==t&&console.warn(`Route name "${t}" has been camelCased to "${this.name}"`),this.url=o(e),this.headers=r||{},this.method=n,this.detectArguments()}static createRoute(t,e,r,n){const a=new d(t,e,r,n);return l.i.registerRoute(a),a}static get(t,e,r={}){return this.createRoute(t,e,r,"GET")}static post(t,e,r){return this.createRoute(t,e,r,"POST")}static put(t,e,r){return this.createRoute(t,e,r,"PUT")}static delete(t,e,r){return this.createRoute(t,e,r,"DELETE")}static patch(t,e,r){return this.createRoute(t,e,r,"PATCH")}static options(t,e,r){return this.createRoute(t,e,r,"OPTIONS")}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const r=e.replace(/\[|]/g,"");this.arguments.add(r)})}}const m={};async function T(i,t,e={},r={}){let n=b(`${i.url}/${t.url}`,t,e),a={};r&&t.method!==g.GET&&(a.body=JSON.stringify(r)),a.headers={"Content-Type":"application/json",...i.headers,...t.headers},a.method=t.method;const{beforeRoute:p,beforeApi:C,beforeUrl:$,beforeConfig:k}=E({route:t,api:i,url:n,config:a});n=$,a=k,i=l.updateApi(C),t=l.updateRoute(p);const w=await fetch(n,a),{afterRoute:y,afterApi:O,afterData:_}=P({route:t,api:i,response:w,data:await w.json()});return l.updateApi(O),l.updateRoute(y),f.run(`${i.name}.${t.name}`),_}function b(i,t,e){let r=i;return t.arguments.forEach(n=>{if(e[n]===void 0)throw new Error(`Argument ${n} is missing`);r=r.replace(`[${n}]`,e[n])}),r}function E({route:i,api:t,url:e,config:r}){var a,p;const n=(p=(a=i.callbacks).before)==null?void 0:p.call(a,{route:i,api:t,url:e,config:r});return{beforeRoute:(n==null?void 0:n.route)||i,beforeApi:(n==null?void 0:n.api)||t,beforeUrl:(n==null?void 0:n.url)||e,beforeConfig:(n==null?void 0:n.config)||r}}function P({route:i,api:t,response:e,data:r}){var a,p;const n=(p=(a=i.callbacks).after)==null?void 0:p.call(a,{route:i,api:t,response:e,data:r});return{afterRoute:(n==null?void 0:n.route)||i,afterApi:(n==null?void 0:n.api)||t,afterResponse:(n==null?void 0:n.response)||e,afterData:(n==null?void 0:n.data)||r}}const u=class u{constructor(){c(this,"_apis",new Map);c(this,"_currentApi",null)}static get i(){return u._instance||(u._instance=new u),u._instance}registerApi(t){this._apis.set(t.name,t),m[t.name]={}}setCurrent(t){const e=this._apis.get(t);if(!e)throw new Error(`API ${t} not found`);this._currentApi=e}clearCurrent(){this._currentApi=null}registerRoute(t){if(!this._currentApi)throw new Error("No current API set, use Route only inside Api.create callback");t.api=this._currentApi.name,this._currentApi.routes.set(t.name,t),this.addToKlaimRoute(t.api,t)}getApi(t){return this._apis.get(t)}getRoute(t,e){const r=this._apis.get(t);if(!r)throw new Error(`API ${t} not found`);return r.routes.get(e)}static updateApi(t){return u.i._apis.has(t.name)||u.i.registerApi(t),u.i._apis.set(t.name,t),t}static updateRoute(t){const e=u.i._apis.get(t.api);if(!e)throw new Error(`API ${t.api} not found`);return e.routes.set(t.name,t),t}addToKlaimRoute(t,e){m[t][e.name]=async(r={},n={})=>{const a=u.i._apis.get(t);if(!a)throw new Error(`API ${e.api} not found`);return T(a,e,r,n)}}};c(u,"_instance");let l=u;class A{constructor(t,e,r={}){c(this,"name");c(this,"url");c(this,"headers");c(this,"routes",new Map);this.name=t,this.url=o(e),this.headers=r}static create(t,e,r,n={}){const a=h(t);a!==t&&console.warn(`API name "${t}" has been camelCased to "${a}"`);const p=new A(a,e,n);return l.i.registerApi(p),l.i.setCurrent(a),r(),l.i.clearCurrent(),p}}s.Api=A,s.Hook=f,s.Klaim=m,s.Registry=l,s.Route=d,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
1
+ (function(c,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(c=typeof globalThis<"u"?globalThis:c||self,o(c.klaim={}))})(this,function(c){"use strict";var j=Object.defineProperty;var N=(c,o,p)=>o in c?j(c,o,{enumerable:!0,configurable:!0,writable:!0,value:p}):c[o]=p;var s=(c,o,p)=>N(c,typeof o!="symbol"?o+"":o,p);function o(r){return r.trim().replace(/^\/|\/$/g,"")}function p(r){return r.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}const f=class f{constructor(){s(this,"cache");this.cache=new Map}static get i(){return f._instance||(f._instance=new f),f._instance}set(t,e,a){const n=Date.now()+a;this.cache.set(t,{data:e,expiry:n})}has(t){const e=this.cache.get(t);return e?Date.now()>e.expiry?(this.cache.delete(t),!1):!0:!1}get(t){return this.has(t)?this.cache.get(t).data:null}};s(f,"_instance");let d=f;function E(r){let a=2166136261;for(let i=0;i<r.length;i++)a^=r.charCodeAt(i),a*=16777619;let n=(a>>>0).toString(16).padStart(8,"0");for(;n.length<32;)a^=n.charCodeAt(n.length%n.length),a*=16777619,n+=(a>>>0).toString(16).padStart(8,"0");return n.substring(0,32)}async function C(r,t,e){const a=`${r.toString()}${JSON.stringify(t)}`,n=E(a);if(d.i.has(n))return d.i.get(n);const h=await(await fetch(r,t)).json();return d.i.set(n,h,e),h}class m{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}s(m,"_callbacks",new Map);var b=(r=>(r.GET="GET",r.POST="POST",r.PUT="PUT",r.DELETE="DELETE",r.PATCH="PATCH",r.OPTIONS="OPTIONS",r))(b||{});class w{constructor(t,e,a,n="GET"){s(this,"api","undefined");s(this,"name");s(this,"url");s(this,"method");s(this,"headers");s(this,"arguments",new Set);s(this,"callbacks",{before:null,after:null});this.name=p(t),this.name!==t&&console.warn(`Route name "${t}" has been camelCased to "${this.name}"`),this.url=o(e),this.headers=a||{},this.method=n,this.detectArguments()}static createRoute(t,e,a,n){const i=new w(t,e,a,n);return l.i.registerRoute(i),i}static get(t,e,a={}){return this.createRoute(t,e,a,"GET")}static post(t,e,a){return this.createRoute(t,e,a,"POST")}static put(t,e,a){return this.createRoute(t,e,a,"PUT")}static delete(t,e,a){return this.createRoute(t,e,a,"DELETE")}static patch(t,e,a){return this.createRoute(t,e,a,"PATCH")}static options(t,e,a){return this.createRoute(t,e,a,"OPTIONS")}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace(/\[|]/g,"");this.arguments.add(a)})}withCache(t=20){return this.cache=t,this}}const A={};async function P(r,t,e={},a={}){let n=S(`${r.url}/${t.url}`,t,e),i={};a&&t.method!==b.GET&&(i.body=JSON.stringify(a)),i.headers={"Content-Type":"application/json",...r.headers,...t.headers},i.method=t.method;const{beforeRoute:h,beforeApi:O,beforeUrl:k,beforeConfig:_}=$({route:t,api:r,url:n,config:i});n=k,i=_,r=l.updateApi(O),t=l.updateRoute(h);const I=r.cache||t.cache;let g;I?g=await C(n,i,r.cache):g=await(await fetch(n,i)).json();const{afterRoute:U,afterApi:D,afterData:H}=y({route:t,api:r,response:g,data:g});return l.updateApi(D),l.updateRoute(U),m.run(`${r.name}.${t.name}`),H}function S(r,t,e){let a=r;return t.arguments.forEach(n=>{if(e[n]===void 0)throw new Error(`Argument ${n} is missing`);a=a.replace(`[${n}]`,e[n])}),a}function $({route:r,api:t,url:e,config:a}){var i,h;const n=(h=(i=r.callbacks).before)==null?void 0:h.call(i,{route:r,api:t,url:e,config:a});return{beforeRoute:(n==null?void 0:n.route)||r,beforeApi:(n==null?void 0:n.api)||t,beforeUrl:(n==null?void 0:n.url)||e,beforeConfig:(n==null?void 0:n.config)||a}}function y({route:r,api:t,response:e,data:a}){var i,h;const n=(h=(i=r.callbacks).after)==null?void 0:h.call(i,{route:r,api:t,response:e,data:a});return{afterRoute:(n==null?void 0:n.route)||r,afterApi:(n==null?void 0:n.api)||t,afterResponse:(n==null?void 0:n.response)||e,afterData:(n==null?void 0:n.data)||a}}const u=class u{constructor(){s(this,"_apis",new Map);s(this,"_currentApi",null)}static get i(){return u._instance||(u._instance=new u),u._instance}registerApi(t){this._apis.set(t.name,t),A[t.name]={}}setCurrent(t){const e=this._apis.get(t);if(!e)throw new Error(`API ${t} not found`);this._currentApi=e}clearCurrent(){this._currentApi=null}registerRoute(t){if(!this._currentApi)throw new Error("No current API set, use Route only inside Api.create callback");t.api=this._currentApi.name,this._currentApi.routes.set(t.name,t),this.addToKlaimRoute(t.api,t)}getApi(t){return this._apis.get(t)}getRoute(t,e){const a=this._apis.get(t);if(!a)throw new Error(`API ${t} not found`);return a.routes.get(e)}static updateApi(t){return u.i._apis.has(t.name)||u.i.registerApi(t),u.i._apis.set(t.name,t),t}static updateRoute(t){const e=u.i._apis.get(t.api);if(!e)throw new Error(`API ${t.api} not found`);return e.routes.set(t.name,t),t}addToKlaimRoute(t,e){A[t][e.name]=async(a={},n={})=>{const i=u.i._apis.get(t);if(!i)throw new Error(`API ${e.api} not found`);return P(i,e,a,n)}}};s(u,"_instance");let l=u;class T{constructor(t,e,a={}){s(this,"name");s(this,"url");s(this,"headers");s(this,"routes",new Map);s(this,"cache",!1);this.name=t,this.url=o(e),this.headers=a}static create(t,e,a,n={}){const i=p(t);i!==t&&console.warn(`API name "${t}" has been camelCased to "${i}"`);const h=new T(i,e,n);return l.i.registerApi(h),l.i.setCurrent(i),a(),l.i.clearCurrent(),h}withCache(t=20){return this.cache=t,this}}c.Api=T,c.Hook=m,c.Klaim=A,c.Registry=l,c.Route=w,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "homepage": "https://github.com/antharuu/klaim#readme",
22
22
  "type": "module",
23
- "version": "1.3.0",
23
+ "version": "1.4.0",
24
24
  "main": "dist/klaim.cjs.js",
25
25
  "module": "dist/klaim.es.js",
26
26
  "types": "dist/index.d.ts",
package/src/core/Api.ts CHANGED
@@ -9,6 +9,9 @@ interface IApi {
9
9
  url: string;
10
10
  headers: IHeaders;
11
11
  routes: Map<string, Route>;
12
+ cache: false | number;
13
+
14
+ withCache: (duration?: number) => this;
12
15
  }
13
16
 
14
17
  export type IApiCallback = () => void;
@@ -26,6 +29,8 @@ export class Api implements IApi {
26
29
 
27
30
  public routes: Map<string, Route> = new Map<string, Route>();
28
31
 
32
+ public cache: false | number = false;
33
+
29
34
  /**
30
35
  * Constructor
31
36
  *
@@ -65,4 +70,15 @@ export class Api implements IApi {
65
70
  Registry.i.clearCurrent();
66
71
  return api;
67
72
  }
73
+
74
+ /**
75
+ * Enables caching for the API
76
+ *
77
+ * @param duration - The duration to cache the response for seconds (default: 20)
78
+ * @returns The API
79
+ */
80
+ public withCache (duration = 20): this {
81
+ this.cache = duration;
82
+ return this;
83
+ }
68
84
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A simple cache implementation that stores key-value pairs with a time-to-live (TTL) in milliseconds.
3
+ */
4
+ export class Cache {
5
+ private static _instance: Cache;
6
+
7
+ private cache: Map<string, { data: any; expiry: number }>;
8
+
9
+ /**
10
+ * Private constructor to prevent instantiation from outside the class.
11
+ */
12
+ private constructor () {
13
+ this.cache = new Map();
14
+ }
15
+
16
+ /**
17
+ * Provides access to the singleton instance of the class.
18
+ *
19
+ * @returns The singleton instance of Cache.
20
+ */
21
+ public static get i (): Cache {
22
+ if (!Cache._instance) {
23
+ Cache._instance = new Cache();
24
+ }
25
+ return Cache._instance;
26
+ }
27
+
28
+ /**
29
+ * Sets the cache for a specific key.
30
+ *
31
+ * @param key The key to cache the value under.
32
+ * @param value The value to be cached.
33
+ * @param ttl Time to live in milliseconds.
34
+ */
35
+ public set (key: string, value: any, ttl: number): void {
36
+ const expiry = Date.now() + ttl;
37
+ this.cache.set(key, { data: value, expiry });
38
+ }
39
+
40
+ /**
41
+ * Checks if the cache has a valid entry for the given key.
42
+ *
43
+ * @param key The key to check in the cache.
44
+ * @returns True if a valid cache entry exists, otherwise false.
45
+ */
46
+ public has (key: string): boolean {
47
+ const cacheEntry = this.cache.get(key);
48
+ if (!cacheEntry) {
49
+ return false;
50
+ }
51
+
52
+ if (Date.now() > cacheEntry.expiry) {
53
+ this.cache.delete(key);
54
+ return false;
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Gets the cached value for the given key.
62
+ *
63
+ * @param key The key to retrieve from the cache.
64
+ * @returns The cached value or null if not found or expired.
65
+ */
66
+ public get (key: string): any | null {
67
+ if (this.has(key)) {
68
+ return this.cache.get(key)!.data;
69
+ }
70
+ return null;
71
+ }
72
+ }
package/src/core/Klaim.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import fetchWithCache from "../tools/fetchWithCache.ts";
2
+
1
3
  import { Api } from "./Api";
2
4
  import { Hook } from "./Hook";
3
5
  import { Registry } from "./Registry";
@@ -7,7 +9,10 @@ export type IArgs = Record<string, unknown>;
7
9
 
8
10
  export type IBody = Record<string, unknown>;
9
11
 
10
- export type IRouteReference = Record<string, <T>(args?: IArgs, body?: IBody) => Promise<T>>;
12
+ export type IRouteReference = Record<
13
+ string,
14
+ <T>(args?: IArgs, body?: IBody) => Promise<T>
15
+ >;
11
16
 
12
17
  export type IApiReference = Record<string, IRouteReference>;
13
18
 
@@ -22,7 +27,12 @@ export const Klaim: IApiReference = {};
22
27
  * @param body - The body to pass to the route
23
28
  * @returns The response
24
29
  */
25
- export async function callApi<T> (api: Api, route: Route, args: IArgs = {}, body: IBody = {}): Promise<T> {
30
+ export async function callApi<T> (
31
+ api: Api,
32
+ route: Route,
33
+ args: IArgs = {},
34
+ body: IBody = {}
35
+ ): Promise<T> {
26
36
  let url = applyArgs(`${api.url}/${route.url}`, route, args);
27
37
 
28
38
  let config: Record<string, unknown> = {};
@@ -50,13 +60,21 @@ export async function callApi<T> (api: Api, route: Route, args: IArgs = {}, body
50
60
  api = Registry.updateApi(beforeApi);
51
61
  route = Registry.updateRoute(beforeRoute);
52
62
 
53
- const response = await fetch(url, config);
63
+ const withCache = api.cache || route.cache;
64
+
65
+ let response;
66
+ if (withCache) {
67
+ response = await fetchWithCache(url, config, api.cache);
68
+ } else {
69
+ const rawResponse = await fetch(url, config);
70
+ response = await rawResponse.json();
71
+ }
54
72
 
55
73
  const {
56
74
  afterRoute,
57
75
  afterApi,
58
76
  afterData
59
- } = applyAfter({ route, api, response, data: await response.json() });
77
+ } = applyAfter({ route, api, response, data: response });
60
78
  Registry.updateApi(afterApi);
61
79
  Registry.updateRoute(afterRoute);
62
80
 
@@ -81,7 +99,7 @@ function applyArgs (url: string, route: Route, args: IArgs): string {
81
99
  throw new Error(`Argument ${arg} is missing`);
82
100
  }
83
101
 
84
- newUrl = newUrl.replace(`[${arg}]`, <string>args[arg]);
102
+ newUrl = newUrl.replace(`[${arg}]`, <string> args[arg]);
85
103
  });
86
104
 
87
105
  return newUrl;
package/src/core/Route.ts CHANGED
@@ -28,8 +28,8 @@ export interface ICallbackAfter {
28
28
  }
29
29
 
30
30
  interface IRouteCallbacks {
31
- before: ((args: ICallbackBefore) => (Partial<ICallbackBefore> | void)) | null;
32
- after: ((args: ICallbackAfter) => (Partial<ICallbackAfter> | void)) | null;
31
+ before: ((args: ICallbackBefore) => Partial<ICallbackBefore> | void) | null;
32
+ after: ((args: ICallbackAfter) => Partial<ICallbackAfter> | void) | null;
33
33
  }
34
34
 
35
35
  interface IRoute {
@@ -40,9 +40,16 @@ interface IRoute {
40
40
  headers: IHeaders;
41
41
  arguments: Set<string>;
42
42
  callbacks: IRouteCallbacks;
43
+ cache: false | number;
43
44
 
44
- before: (callback: (args: ICallbackBefore) => (Partial<ICallbackBefore> | void)) => Route;
45
- after: (callback: (args: ICallbackAfter) => (Partial<ICallbackAfter> | void)) => Route;
45
+ before: (
46
+ callback: (args: ICallbackBefore) => Partial<ICallbackBefore> | void,
47
+ ) => Route;
48
+ after: (
49
+ callback: (args: ICallbackAfter) => Partial<ICallbackAfter> | void,
50
+ ) => Route;
51
+
52
+ withCache: (duration?: number) => this;
46
53
  }
47
54
 
48
55
  /**
@@ -80,7 +87,12 @@ export class Route implements IRoute {
80
87
  * @param headers - The headers to be sent with the request
81
88
  * @param method - The HTTP method of the route
82
89
  */
83
- private constructor (name: string, url: string, headers: IHeaders, method: RouteMethod = RouteMethod.GET) {
90
+ private constructor (
91
+ name: string,
92
+ url: string,
93
+ headers: IHeaders,
94
+ method: RouteMethod = RouteMethod.GET
95
+ ) {
84
96
  this.name = toCamelCase(name);
85
97
  if (this.name !== name) {
86
98
  console.warn(`Route name "${name}" has been camelCased to "${this.name}"`);
@@ -102,7 +114,12 @@ export class Route implements IRoute {
102
114
  * @param method - The HTTP method of the route
103
115
  * @returns The new route
104
116
  */
105
- private static createRoute (name: string, url: string, headers: IHeaders, method: RouteMethod): Route {
117
+ private static createRoute (
118
+ name: string,
119
+ url: string,
120
+ headers: IHeaders,
121
+ method: RouteMethod
122
+ ): Route {
106
123
  const route = new Route(name, url, headers, method);
107
124
  Registry.i.registerRoute(route as Route);
108
125
  return route;
@@ -116,7 +133,11 @@ export class Route implements IRoute {
116
133
  * @param headers - The headers to be sent with the request
117
134
  * @returns The new route
118
135
  */
119
- public static get (name: string, url: string, headers: IHeaders = {}): Route {
136
+ public static get (
137
+ name: string,
138
+ url: string,
139
+ headers: IHeaders = {}
140
+ ): Route {
120
141
  return this.createRoute(name, url, headers, RouteMethod.GET);
121
142
  }
122
143
 
@@ -186,7 +207,7 @@ export class Route implements IRoute {
186
207
  * @param callback - The callback
187
208
  * @returns The route
188
209
  */
189
- public before (callback: (args: ICallbackBefore) => (Partial<ICallbackBefore> | void)): this {
210
+ public before (callback: (args: ICallbackBefore) => Partial<ICallbackBefore> | void): this {
190
211
  this.callbacks.before = callback;
191
212
  return this;
192
213
  }
@@ -197,7 +218,7 @@ export class Route implements IRoute {
197
218
  * @param callback - The callback
198
219
  * @returns The route
199
220
  */
200
- public after (callback: (args: ICallbackAfter) => (Partial<ICallbackAfter> | void)): this {
221
+ public after (callback: (args: ICallbackAfter) => Partial<ICallbackAfter> | void): this {
201
222
  this.callbacks.after = callback;
202
223
  return this;
203
224
  }
@@ -214,4 +235,15 @@ export class Route implements IRoute {
214
235
  });
215
236
  }
216
237
  }
238
+
239
+ /**
240
+ * Enables caching for the Route
241
+ *
242
+ * @param duration - The duration to cache the response for seconds (default: 20)
243
+ * @returns The Route
244
+ */
245
+ public withCache (duration = 20): this {
246
+ this.cache = duration;
247
+ return this;
248
+ }
217
249
  }
@@ -0,0 +1,30 @@
1
+ import { Cache } from "../core/Cache.ts";
2
+
3
+ import hashStr from "./hashStr.ts";
4
+
5
+ /**
6
+ * Fetch with cache
7
+ *
8
+ * @param input - The input
9
+ * @param init - The init
10
+ * @param ttl - The time to live
11
+ * @returns The response
12
+ */
13
+ export default async function (
14
+ input: string | URL | globalThis.Request,
15
+ init?: RequestInit,
16
+ ttl?: number
17
+ ): Promise<Response> {
18
+ const baseString = `${input.toString()}${JSON.stringify(init)}`;
19
+ const cacheKey = hashStr(baseString);
20
+
21
+ if (Cache.i.has(cacheKey)) {
22
+ return Cache.i.get(cacheKey);
23
+ }
24
+
25
+ const response = await fetch(input, init);
26
+ const data = await response.json();
27
+
28
+ Cache.i.set(cacheKey, data, ttl);
29
+ return data;
30
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Calculates the hash value of a given string.
3
+ *
4
+ * @param {string} str - The string to calculate the hash for.
5
+ * @returns {string} The hash value of the input string.
6
+ */
7
+ export default function (str: string): string {
8
+ const initialHash = 0x811c9dc5;
9
+ const prime = 0x01000193;
10
+ let hash = initialHash;
11
+
12
+ for (let i = 0; i < str.length; i++) {
13
+ hash ^= str.charCodeAt(i);
14
+ hash *= prime;
15
+ }
16
+
17
+ // Convert the integer hash to a hexadecimal string
18
+ let hexHash = (hash >>> 0).toString(16).padStart(8, "0");
19
+
20
+ // Extend the hash to 32 characters by rehashing and concatenating
21
+ while (hexHash.length < 32) {
22
+ hash ^= hexHash.charCodeAt(hexHash.length % hexHash.length);
23
+ hash *= prime;
24
+ hexHash += (hash >>> 0).toString(16).padStart(8, "0");
25
+ }
26
+
27
+ return hexHash.substring(0, 32);
28
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Api, Klaim, Registry, Route } from "../src/index.ts";
3
+
4
+ const apiName = "testApi";
5
+ const apiUrl = "https://lorem-json.com/api/";
6
+
7
+ const routeName = "testRoute";
8
+ const routeUrl = "/json";
9
+ const routeBody = {
10
+ "name": "{{name()}}",
11
+ };
12
+
13
+ await describe("Cache", async () => {
14
+ it("should not cache the API response", async () => {
15
+ Api.create(apiName, apiUrl, () => {
16
+ Route.post(routeName, routeUrl);
17
+ });
18
+
19
+ const { name: nameA } = await Klaim[apiName][routeName]({}, routeBody);
20
+ const { name: nameB } = await Klaim[apiName][routeName]({}, routeBody);
21
+
22
+ expect(nameA).not.toEqual(nameB);
23
+ });
24
+
25
+ it("should keep the API response in cache", async () => {
26
+ Api.create(apiName, apiUrl, () => {
27
+ Route.post(routeName, routeUrl);
28
+ }).withCache();
29
+
30
+ const { name: nameA } = await Klaim[apiName][routeName]({}, routeBody);
31
+ const { name: nameB } = await Klaim[apiName][routeName]({}, routeBody);
32
+
33
+ expect(nameA).toEqual(nameB);
34
+ });
35
+
36
+ it("should keep the route response in cache", async () => {
37
+ Api.create(apiName, apiUrl, () => {
38
+ Route.post(routeName, routeUrl).withCache();
39
+ });
40
+
41
+ const { name: nameA } = await Klaim[apiName][routeName]({}, routeBody);
42
+ const { name: nameB } = await Klaim[apiName][routeName]({}, routeBody);
43
+
44
+ expect(nameA).toEqual(nameB);
45
+ });
46
+ });