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 +39 -0
- package/bun.lockb +0 -0
- package/deno.json +38 -38
- package/dist/klaim.cjs +1 -1
- package/dist/klaim.es.js +159 -72
- package/dist/klaim.umd.js +1 -1
- package/package.json +1 -1
- package/src/core/Api.ts +16 -0
- package/src/core/Cache.ts +72 -0
- package/src/core/Klaim.ts +23 -5
- package/src/core/Route.ts +41 -9
- package/src/tools/fetchWithCache.ts +30 -0
- package/src/tools/hashStr.ts +28 -0
- package/tests/05.cache.test.ts +46 -0
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.
|
|
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
|
|
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
|
|
2
|
-
var _ = (
|
|
3
|
-
var i = (
|
|
4
|
-
function
|
|
5
|
-
return
|
|
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
|
|
8
|
-
return
|
|
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
|
|
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(
|
|
31
|
-
var
|
|
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,
|
|
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 =
|
|
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,
|
|
70
|
-
const s = new A(t, e, n,
|
|
71
|
-
return
|
|
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
|
|
205
|
-
async function
|
|
206
|
-
let
|
|
207
|
-
n && t.method !==
|
|
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
|
-
...
|
|
283
|
+
...r.headers,
|
|
210
284
|
...t.headers
|
|
211
285
|
}, s.method = t.method;
|
|
212
286
|
const {
|
|
213
|
-
beforeRoute:
|
|
214
|
-
beforeApi:
|
|
215
|
-
beforeUrl:
|
|
216
|
-
beforeConfig:
|
|
217
|
-
} =
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
227
|
-
let n =
|
|
228
|
-
return t.arguments.forEach((
|
|
229
|
-
if (e[
|
|
230
|
-
throw new Error(`Argument ${
|
|
231
|
-
n = n.replace(`[${
|
|
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
|
|
235
|
-
var s,
|
|
236
|
-
const
|
|
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: (
|
|
239
|
-
beforeApi: (
|
|
240
|
-
beforeUrl: (
|
|
241
|
-
beforeConfig: (
|
|
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
|
|
245
|
-
var s,
|
|
246
|
-
const
|
|
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: (
|
|
249
|
-
afterApi: (
|
|
250
|
-
afterResponse: (
|
|
251
|
-
afterData: (
|
|
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
|
|
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
|
|
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),
|
|
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
|
|
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 =
|
|
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
|
-
|
|
356
|
-
const s =
|
|
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
|
|
436
|
+
return U(s, e, n, a);
|
|
360
437
|
};
|
|
361
438
|
}
|
|
362
439
|
};
|
|
363
|
-
i(
|
|
364
|
-
let
|
|
365
|
-
class
|
|
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
|
|
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,
|
|
390
|
-
const s =
|
|
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
|
|
393
|
-
return
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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(
|
|
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
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<
|
|
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> (
|
|
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
|
|
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:
|
|
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) =>
|
|
32
|
-
after: ((args: ICallbackAfter) =>
|
|
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: (
|
|
45
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
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
|
+
});
|