klaim 1.8.87 → 1.9.1

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
@@ -6,19 +6,20 @@
6
6
  - [Next features](#-next-features)
7
7
  - [Installation](#-installation)
8
8
  - [Usage](#-usage)
9
- - [Basic API Configuration](#basic-api-configuration)
10
- - [Route Definition](#route-definition)
11
- - [Request Handling](#request-handling)
12
- - [Groups](#groups)
13
- - [API Groups](#api-groups)
14
- - [Route Groups](#route-groups)
15
- - [Nested Groups](#nested-groups)
16
- - [Group Configuration](#group-configuration)
17
- - [Middleware Usage](#middleware-usage)
18
- - [Hook Subscription](#hook-subscription)
19
- - [Caching Requests](#caching-requests)
20
- - [Retry Mechanism](#retry-mechanism)
21
- - [Response Validation](#response-validation)
9
+ - [Basic API Configuration](#basic-api-configuration)
10
+ - [Route Definition](#route-definition)
11
+ - [Request Handling](#request-handling)
12
+ - [Groups](#groups)
13
+ - [API Groups](#api-groups)
14
+ - [Route Groups](#route-groups)
15
+ - [Nested Groups](#nested-groups)
16
+ - [Group Configuration](#group-configuration)
17
+ - [Middleware Usage](#middleware-usage)
18
+ - [Hook Subscription](#hook-subscription)
19
+ - [Caching Requests](#caching-requests)
20
+ - [Retry Mechanism](#retry-mechanism)
21
+ - [Rate Limiting](#rate-limiting)
22
+ - [Response Validation](#response-validation)
22
23
  - [Links](#-links)
23
24
  - [Contributing](#-contributing)
24
25
  - [License](#-license)
@@ -35,13 +36,13 @@
35
36
  - **Hook System**: Subscribe to hooks to monitor and react to specific events.
36
37
  - **Caching**: Enable caching on requests to reduce network load and improve performance.
37
38
  - **Retry Mechanism**: Automatically retry failed requests to enhance reliability.
39
+ - **Rate Limiting**: Control the frequency of API calls to prevent abuse and respect API provider limits.
38
40
  - **TypeScript Support**: Fully typed for enhanced code quality and developer experience.
39
41
  - **Response Validation**: Validate responses using schemas for increased reliability and consistency.
40
42
  - **Pagination**: Handle paginated requests easily with support for both page and offset based pagination.
41
43
 
42
44
  ## ⌛ Next features
43
45
 
44
- - Rate Limiting (Version: 1.9)
45
46
  - Login (Version: 1.10)
46
47
  - Time Out (Version: 1.11)
47
48
  - Error Handling (Version: 1.12)
@@ -73,18 +74,18 @@ import {Api, Route} from 'klaim';
73
74
 
74
75
  // Your simple Todo type
75
76
  type Todo = {
76
- userId: number;
77
- id: number;
78
- title: string;
79
- completed: boolean;
77
+ userId: number;
78
+ id: number;
79
+ title: string;
80
+ completed: boolean;
80
81
  };
81
82
 
82
83
  // Create a new API with the name "hello" and the base URL "https://jsonplaceholder.typicode.com/"
83
84
  Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
84
- // Define routes for the API
85
- Route.get<Todo[]>("listTodos", "todos");
86
- Route.get<Todo>("getTodo", "todos/[id]");
87
- Route.post<Todo>("addTodo", "todos");
85
+ // Define routes for the API
86
+ Route.get<Todo[]>("listTodos", "todos");
87
+ Route.get<Todo>("getTodo", "todos/[id]");
88
+ Route.post<Todo>("addTodo", "todos");
88
89
  });
89
90
  ```
90
91
 
@@ -95,28 +96,28 @@ custom configurations:
95
96
 
96
97
  ```typescript
97
98
  Api.create("api", "https://api.example.com", () => {
98
- // Basic GET route
99
- Route.get("listUsers", "/users");
99
+ // Basic GET route
100
+ Route.get("listUsers", "/users");
100
101
 
101
- // GET route with URL parameter
102
- Route.get("getUser", "/users/[id]");
102
+ // GET route with URL parameter
103
+ Route.get("getUser", "/users/[id]");
103
104
 
104
- // POST route with custom headers and body
105
- Route.post("createUser", "/users", {
106
- "Content-Type": "application/json"
107
- }, {userId: 1, name: "John Doe"});
105
+ // POST route with custom headers and body
106
+ Route.post("createUser", "/users", {
107
+ "Content-Type": "application/json"
108
+ }, {userId: 1, name: "John Doe"});
108
109
 
109
- // PUT route with parameter
110
- Route.put("updateUser", "/users/[id]");
110
+ // PUT route with parameter
111
+ Route.put("updateUser", "/users/[id]");
111
112
 
112
- // DELETE route
113
- Route.delete("deleteUser", "/users/[id]");
113
+ // DELETE route
114
+ Route.delete("deleteUser", "/users/[id]");
114
115
 
115
- // PATCH route
116
- Route.patch("updateUserStatus", "/users/[id]/status");
116
+ // PATCH route
117
+ Route.patch("updateUserStatus", "/users/[id]/status");
117
118
 
118
- // OPTIONS route
119
- Route.options("userOptions", "/users");
119
+ // OPTIONS route
120
+ Route.options("userOptions", "/users");
120
121
  });
121
122
  ```
122
123
 
@@ -134,17 +135,17 @@ import {Group, Api, Route} from 'klaim';
134
135
 
135
136
  // Create a group for user-related services
136
137
  Group.create("userServices", () => {
137
- // Authentication API
138
- Api.create("auth", "https://auth.example.com", () => {
139
- Route.post("login", "/login");
140
- Route.post("register", "/register");
141
- });
142
-
143
- // User Management API
144
- Api.create("users", "https://users.example.com", () => {
145
- Route.get("list", "/users");
146
- Route.get("getOne", "/users/[id]");
147
- });
138
+ // Authentication API
139
+ Api.create("auth", "https://auth.example.com", () => {
140
+ Route.post("login", "/login");
141
+ Route.post("register", "/register");
142
+ });
143
+
144
+ // User Management API
145
+ Api.create("users", "https://users.example.com", () => {
146
+ Route.get("list", "/users");
147
+ Route.get("getOne", "/users/[id]");
148
+ });
148
149
  }).withRetry(3); // Apply retry mechanism to all APIs in the group
149
150
 
150
151
  // Access grouped APIs
@@ -158,18 +159,18 @@ Organize routes within an API into logical groups:
158
159
 
159
160
  ```typescript
160
161
  Api.create("hello", "https://api.example.com/", () => {
161
- // Group user-related routes
162
- Group.create("users", () => {
163
- Route.get<User[]>("list", "/users");
164
- Route.get<User>("getOne", "/users/[id]");
165
- Route.post<User>("create", "/users");
166
- }).withCache(60); // Cache all user routes for 60 seconds
167
-
168
- // Group product-related routes
169
- Group.create("products", () => {
170
- Route.get("list", "/products");
171
- Route.get("getOne", "/products/[id]");
172
- });
162
+ // Group user-related routes
163
+ Group.create("users", () => {
164
+ Route.get<User[]>("list", "/users");
165
+ Route.get<User>("getOne", "/users/[id]");
166
+ Route.post<User>("create", "/users");
167
+ }).withCache(60); // Cache all user routes for 60 seconds
168
+
169
+ // Group product-related routes
170
+ Group.create("products", () => {
171
+ Route.get("list", "/products");
172
+ Route.get("getOne", "/products/[id]");
173
+ });
173
174
  });
174
175
 
175
176
  // Use grouped routes
@@ -183,27 +184,27 @@ Create complex hierarchies with nested groups:
183
184
 
184
185
  ```typescript
185
186
  Group.create("services", () => {
186
- // Internal services group
187
- Group.create("internal", () => {
188
- Api.create("logs", "https://logs.internal.example.com", () => {
189
- Route.post("write", "/logs");
190
- });
191
-
192
- Api.create("metrics", "https://metrics.internal.example.com", () => {
193
- Route.post("track", "/metrics");
194
- });
195
- }).withRetry(5); // More retries for internal services
196
-
197
- // External services group
198
- Group.create("external", () => {
199
- Api.create("weather", "https://api.weather.com", () => {
200
- Route.get("forecast", "/forecast/[city]");
201
- });
202
-
203
- Api.create("geocoding", "https://api.geocoding.com", () => {
204
- Route.get("search", "/search/[query]");
205
- });
206
- }).withCache(300); // Cache external services longer
187
+ // Internal services group
188
+ Group.create("internal", () => {
189
+ Api.create("logs", "https://logs.internal.example.com", () => {
190
+ Route.post("write", "/logs");
191
+ });
192
+
193
+ Api.create("metrics", "https://metrics.internal.example.com", () => {
194
+ Route.post("track", "/metrics");
195
+ });
196
+ }).withRetry(5); // More retries for internal services
197
+
198
+ // External services group
199
+ Group.create("external", () => {
200
+ Api.create("weather", "https://api.weather.com", () => {
201
+ Route.get("forecast", "/forecast/[city]");
202
+ });
203
+
204
+ Api.create("geocoding", "https://api.geocoding.com", () => {
205
+ Route.get("search", "/search/[query]");
206
+ });
207
+ }).withCache(300); // Cache external services longer
207
208
  });
208
209
 
209
210
  // Access nested groups
@@ -217,22 +218,22 @@ Groups can share configuration among all their members:
217
218
 
218
219
  ```typescript
219
220
  Group.create("apis", () => {
220
- Api.create("service1", "https://api1.example.com", () => {
221
- Route.get("test", "/test");
222
- });
221
+ Api.create("service1", "https://api1.example.com", () => {
222
+ Route.get("test", "/test");
223
+ });
223
224
 
224
- Api.create("service2", "https://api2.example.com", () => {
225
- Route.get("test", "/test");
226
- });
225
+ Api.create("service2", "https://api2.example.com", () => {
226
+ Route.get("test", "/test");
227
+ });
227
228
  })
228
- .withCache(60) // Enable caching for all APIs
229
- .withRetry(3) // Enable retries for all APIs
230
- .before(({config}) => { // Add authentication for all APIs
231
- config.headers.Authorization = `Bearer ${getToken()}`;
232
- })
233
- .after(({data}) => { // Process all responses
234
- logResponse(data);
235
- });
229
+ .withCache(60) // Enable caching for all APIs
230
+ .withRetry(3) // Enable retries for all APIs
231
+ .before(({config}) => { // Add authentication for all APIs
232
+ config.headers.Authorization = `Bearer ${getToken()}`;
233
+ })
234
+ .after(({data}) => { // Process all responses
235
+ logResponse(data);
236
+ });
236
237
  ```
237
238
 
238
239
  ### Request Handling
@@ -260,16 +261,16 @@ and `after` middleware to process responses:
260
261
 
261
262
  ```typescript
262
263
  Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
263
- // With before middleware
264
- Route.get<Todo>("getRandomTodo", "todos")
265
- .before(({url}) => {
266
- const random = Math.floor(Math.random() * 10) + 1;
267
- return {url: `${url}/${random}`};
268
- });
269
-
270
- // With after middleware
271
- Route.get<Todo>("getFirstTodo", "todos")
272
- .after(({data: [first]}) => ({data: first}));
264
+ // With before middleware
265
+ Route.get<Todo>("getRandomTodo", "todos")
266
+ .before(({url}) => {
267
+ const random = Math.floor(Math.random() * 10) + 1;
268
+ return {url: `${url}/${random}`};
269
+ });
270
+
271
+ // With after middleware
272
+ Route.get<Todo>("getFirstTodo", "todos")
273
+ .after(({data: [first]}) => ({data: first}));
273
274
  });
274
275
  ```
275
276
 
@@ -283,7 +284,7 @@ import {Hook} from 'klaim';
283
284
 
284
285
  // Subscribe to the "hello.getFirstTodo" hook
285
286
  Hook.subscribe("hello.getFirstTodo", ({url}) => {
286
- console.log(`Requesting ${url}`);
287
+ console.log(`Requesting ${url}`);
287
288
  });
288
289
  ```
289
290
 
@@ -298,14 +299,14 @@ You can enable caching on individual routes:
298
299
 
299
300
  ```typescript
300
301
  Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
301
- // Get a list of todos with default cache duration (20 seconds)
302
- Route.get<Todo[]>("listTodos", "todos").withCache();
302
+ // Get a list of todos with default cache duration (20 seconds)
303
+ Route.get<Todo[]>("listTodos", "todos").withCache();
303
304
 
304
- // Get a specific todo by id with custom cache duration (300 seconds)
305
- Route.get<Todo>("getTodo", "todos/[id]").withCache(300);
305
+ // Get a specific todo by id with custom cache duration (300 seconds)
306
+ Route.get<Todo>("getTodo", "todos/[id]").withCache(300);
306
307
 
307
- // Add a new todo (no cache)
308
- Route.post<Todo>("addTodo", "todos");
308
+ // Add a new todo (no cache)
309
+ Route.post<Todo>("addTodo", "todos");
309
310
  });
310
311
  ```
311
312
 
@@ -317,49 +318,58 @@ You can also enable caching for all routes defined within an API:
317
318
 
318
319
  ```typescript
319
320
  Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
320
- // Define routes for the API
321
- Route.get<Todo[]>("listTodos", "todos");
322
- Route.get<Todo>("getTodo", "todos/[id]");
323
- Route.post<Todo>("addTodo", "todos");
321
+ // Define routes for the API
322
+ Route.get<Todo[]>("listTodos", "todos");
323
+ Route.get<Todo>("getTodo", "todos/[id]");
324
+ Route.post<Todo>("addTodo", "todos");
324
325
  }).withCache(); // Enable default cache duration (20 seconds) for all routes
325
326
  ```
326
327
 
327
328
  ### Retry Mechanism
328
329
 
329
- Automatically retry failed requests to enhance reliability. You can specify the number of retry attempts for individual
330
- routes or for the entire API.
331
-
332
- #### Retry on Individual Routes
333
-
334
- Enable retry on individual routes:
330
+ Configure automatic retries for failed requests:
335
331
 
336
332
  ```typescript
337
- Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
338
- // Get a list of todos with retry mechanism (default: 2)
339
- Route.get<Todo[]>("listTodos", "todos").withRetry();
340
-
341
- // Get a specific todo by id with retry mechanism (specified to 5)
342
- Route.get<Todo>("getTodo", "todos/[id]").withRetry(5);
333
+ // Apply retry at the API level
334
+ Api.create("api", "https://api.example.com", () => {
335
+ Route.get("users", "/users");
336
+ }).withRetry(3); // Will retry failed requests up to 3 times
343
337
 
344
- // Add a new todo (no retry)
345
- Route.post<Todo>("addTodo", "todos");
338
+ // Apply retry at the route level
339
+ Api.create("api", "https://api.example.com", () => {
340
+ Route.get("unstableRoute", "/unstable-endpoint").withRetry(5); // Will retry up to 5 times
346
341
  });
347
342
  ```
348
343
 
349
- #### Retry the Entire API
344
+ ### Rate Limiting
350
345
 
351
- Enable retry for all routes defined within an API:
346
+ Control the frequency of API calls to prevent abuse and respect API provider rate limits:
352
347
 
353
348
  ```typescript
354
- Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
355
- // Define routes for the API
356
- Route.get<Todo[]>("listTodos", "todos");
357
- Route.get<Todo>("getTodo", "todos/[id]");
358
- Route.post<Todo>("addTodo", "todos");
359
- }).withRetry();
360
- ```
349
+ // Apply rate limiting at the API level
350
+ Api.create("api", "https://api.example.com", () => {
351
+ Route.get("users", "/users");
352
+ Route.get("posts", "/posts");
353
+ }).withRate({ limit: 5, duration: 10 }); // Max 5 requests every 10 seconds for this API
354
+
355
+ // Apply rate limiting at the route level
356
+ Api.create("api", "https://api.example.com", () => {
357
+ // This route has its own stricter limits
358
+ Route.get("expensive", "/expensive-operation").withRate({ limit: 2, duration: 60 }); // Max 2 requests per minute
359
+
360
+ // This route uses the default limits (5 per 10 seconds if not specified)
361
+ Route.get("normal", "/normal-operation").withRate();
362
+ });
361
363
 
362
- Now, when a request fails, it will be retried the specified number of times before ultimately failing.
364
+ // Handling rate limit errors
365
+ try {
366
+ await Klaim.api.expensive();
367
+ } catch (error) {
368
+ if (error.message.includes('Rate limit exceeded')) {
369
+ console.log('Please wait before trying again');
370
+ }
371
+ }
372
+ ```
363
373
 
364
374
  ### Response Validation
365
375
 
@@ -378,15 +388,15 @@ import * as yup from 'yup';
378
388
 
379
389
  // Define the schema using Yup
380
390
  const todoSchema = yup.object().shape({
381
- userId: yup.number().required(),
382
- id: yup.number().min(1).max(10).required(),
383
- title: yup.string().required(),
384
- completed: yup.boolean().required()
391
+ userId: yup.number().required(),
392
+ id: yup.number().min(1).max(10).required(),
393
+ title: yup.string().required(),
394
+ completed: yup.boolean().required()
385
395
  });
386
396
 
387
397
  Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
388
- // Get a specific todo by id with validation
389
- Route.get<Todo>("getTodo", "todos/[id]").validate(todoSchema);
398
+ // Get a specific todo by id with validation
399
+ Route.get<Todo>("getTodo", "todos/[id]").validate(todoSchema);
390
400
  });
391
401
 
392
402
  // This request will fail because the id is out of range
@@ -403,12 +413,12 @@ Configure pagination for routes that require it:
403
413
  ```typescript
404
414
  // Basic usage with custom limit and offset parameter
405
415
  Api.create("api", "https://api.example.com", () => {
406
- Route.get("list", "/items").withPagination({
407
- limit: 20, // Items per page
408
- page: 1, // Default page number
409
- pageParam: "offset", // Parameter name for page/offset or any other custom parameter
410
- limitParam: "limit" // Parameter name for limit
411
- }); // All options are optional
416
+ Route.get("list", "/items").withPagination({
417
+ limit: 20, // Items per page
418
+ page: 1, // Default page number
419
+ pageParam: "offset", // Parameter name for page/offset or any other custom parameter
420
+ limitParam: "limit" // Parameter name for limit
421
+ }); // All options are optional
412
422
  });
413
423
 
414
424
  // Using paginated endpoints
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antharuu/klaim",
3
- "version": "1.8.87",
3
+ "version": "1.9.1",
4
4
  "description": "Klaim is a lightweight TypeScript library designed to manage APIs and record requests, optimized for an optimal user experience.",
5
5
  "repository": {
6
6
  "type": "git",
package/dist/klaim.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var O=Object.defineProperty;var U=(n,t,e)=>t in n?O(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var o=(n,t,e)=>U(n,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function E(n){return n.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}function D(n){return n.trim().replace(/^\/|\/$/g,"")}const j={page:1,pageParam:"page",limit:10,limitParam:"limit"};class ${constructor(t,e,a,r={}){o(this,"type");o(this,"name");o(this,"url");o(this,"headers");o(this,"parent");o(this,"method");o(this,"arguments",new Set);o(this,"schema");o(this,"pagination");o(this,"callbacks",{before:null,after:null,call:null});o(this,"cache",!1);o(this,"retry",!1);o(this,"withCache",(t=20)=>(this.cache=t,this));o(this,"withRetry",(t=2)=>(this.retry=t,this));this.type=t,this.name=E(e),this.name!==e&&console.warn(`Name "${e}" has been camelCased to "${this.name}"`),this.url=D(a),this.headers=r||{}}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}onCall(t){return this.callbacks.call=t,this}withPagination(t={}){return this.pagination={...j,...t},this}}const p=class p{constructor(){o(this,"cache");this.cache=new Map}static get i(){return p._instance||(p._instance=new p),p._instance}set(t,e,a=0){const r=Date.now()+a;this.cache.set(t,{data:e,expiry:r})}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}};o(p,"_instance");let d=p;function x(n){let a=2166136261;for(let i=0;i<n.length;i++)a^=n.charCodeAt(i),a*=16777619;let r=(a>>>0).toString(16).padStart(8,"0");for(;r.length<32;)a^=r.charCodeAt(r.length%r.length),a*=16777619,r+=(a>>>0).toString(16).padStart(8,"0");return r.substring(0,32)}async function H(n,t,e){const a=`${n.toString()}${JSON.stringify(t)}`,r=x(a);if(d.i.has(r))return d.i.get(r);const c=await(await fetch(n,t)).json();return d.i.set(r,c,e),c}class A{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}o(A,"_callbacks",new Map);const C={};function I(n,t){return async(...e)=>{if(t.pagination){const[i=0,c={},l={}]=e;return F(n,t,i,c,l)}const[a={},r={}]=e;return F(n,t,void 0,a,r)}}async function F(n,t,e,a={},r={}){const i=n.split(".");let c;for(let g=0;g<i.length;g++){const y=i[g];if(c=s.i.getApi(y),c)break}if(!t||!c||t.type!=="route"||c.type!=="api")throw new Error(`Invalid path: ${n}.${t.name}`);let l=W(`${c.url}/${t.url}`,t,a);if(t.pagination&&typeof e<"u"){const{pageParam:g="page",limit:y=10,limitParam:K="limit"}=t.pagination,b=new URLSearchParams;b.append(g,String(e)),b.append(K,String(y));const G=l.includes("?")?"&":"?";l=`${l}${G}${b.toString()}`}let u={};r&&t.method!=="GET"&&(u.body=JSON.stringify(r)),u.headers={"Content-Type":"application/json",...c.headers,...t.headers},u.method=t.method;const{beforeRoute:f,beforeApi:P,beforeUrl:m,beforeConfig:v}=J({route:t,api:c,url:l,config:u});l=m,u=v,s.updateElement(P),s.updateElement(f);let w=await M(c,t,l,u);t.schema&&"validate"in t.schema&&(w=await t.schema.validate(w));const{afterRoute:T,afterApi:N,afterData:R}=z({route:t,api:c,response:w,data:w});return s.updateElement(N),s.updateElement(T),A.run(`${c.name}.${t.name}`),R}async function L(n,t,e,a){return n?await H(t,e,a.cache):await(await fetch(t,e)).json()}async function M(n,t,e,a){var f,P;const r=n.cache||t.cache,i=t.retry||n.retry||0;let c,l=!1,u=0;for(;u<=i&&!l;)try{(f=t.callbacks)!=null&&f.call?t.callbacks.call({}):(P=n.callbacks)!=null&&P.call&&n.callbacks.call({}),c=await L(!!r,e,a,n),l=!0}catch(m){if(u++,u>i)throw m.message=`Failed to fetch ${e} after ${i} attempts`,m}return c}function W(n,t,e){let a=n;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);a=a.replace(`[${r}]`,e[r])}),a}function J({route:n,api:t,url:e,config:a}){var i,c;const r=(c=(i=n.callbacks).before)==null?void 0:c.call(i,{route:n,api:t,url:e,config:a});return{beforeRoute:(r==null?void 0:r.route)||n,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||a}}function z({route:n,api:t,response:e,data:a}){var i,c;const r=(c=(i=n.callbacks).after)==null?void 0:c.call(i,{route:n,api:t,response:e,data:a});return{afterRoute:(r==null?void 0:r.route)||n,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||a}}const h=class h{constructor(){o(this,"_elements",new Map);o(this,"_currentParent",null)}static get i(){return h._instance||(h._instance=new h),h._instance}registerElement(t){const e=this._currentParent;e&&(t.parent=this.getFullPath(e));const a=this.getElementKey(t);if(this._elements.set(a,t),t.type==="api"||t.type==="group"){let r=C;if(e){const i=this.getFullPath(e).split(".");for(const c of i)r[c]||(r[c]={}),r=r[c]}r[t.name]||(r[t.name]={})}}getCurrentParent(){return this._currentParent}setCurrentParent(t){const e=this._elements.get(t);if(!e||e.type!=="api"&&e.type!=="group")throw new Error(`Element ${t} not found or not a valid parent type`);this._currentParent=e}clearCurrentParent(){this._currentParent=null}registerRoute(t){if(!this._currentParent)throw new Error("No current parent set, use Route only inside Api or Group create callback");t.parent=this.getFullPath(this._currentParent);const e=this.getElementKey(t);this._elements.set(e,t),this.addToKlaimRoute(t)}addToKlaimRoute(t){if(!t.parent)return;let e=C;const a=t.parent.split(".");for(const r of a)e[r]||(e[r]={}),e=e[r];e[t.name]=I(t.parent,t)}getElementKey(t){return t?t.parent?`${t.parent}.${t.name}`:t.name:""}getFullPath(t){if(!t)return"";if(!t.parent)return t.name;const e=[t.name];let a=t;for(;a.parent;){const r=this._elements.get(a.parent);if(!r)break;e.unshift(r.name),a=r}return e.join(".")}getRoute(t,e){return this._elements.get(`${t}.${e}`)}getChildren(t){const e=[];return this._elements.forEach(a=>{a.parent===t&&e.push(a)}),e}static updateElement(t){return h.i._elements.get(h.i.getElementKey(t))||t}getApi(t){const e=this._elements.get(t);if(!e){for(const[a,r]of this._elements.entries())if(r.type==="api"&&a.endsWith(`.${t}`))return r;return}return e.type==="api"?e:this.findApi(e)}findApi(t){if(!t||!t.parent)return;const e=t.parent.split(".");for(let a=e.length;a>=0;a--){const r=e.slice(0,a).join("."),i=this._elements.get(r);if((i==null?void 0:i.type)==="api")return i}}};o(h,"_instance");let s=h;class k extends ${static create(t,e,a,r={}){const i=E(t);i!==t&&console.warn(`API name "${t}" has been camelCased to "${i}"`);const c=new k(i,e,r),l=s.i.getCurrentParent();s.i.registerElement(c);const u=l?s.i.getFullPath(l):"",f=u?`${u}.${i}`:i;return s.i.setCurrentParent(f),a(),l?s.i.setCurrentParent(s.i.getFullPath(l)):s.i.clearCurrentParent(),c}constructor(t,e,a={}){super("api",t,e,a)}}class S extends ${static create(t,e){const a=E(t),r=s.i.getCurrentParent(),i=r?s.i.getFullPath(r):"",c=i?`${i}.${a}`:a,l=new S(a,"");a!==t&&console.warn(`Group name "${t}" has been camelCased to "${a}"`),s.i.registerElement(l);const u=s.i.getCurrentParent();return s.i.setCurrentParent(c),e(),u?s.i.setCurrentParent(s.i.getFullPath(u)):s.i.clearCurrentParent(),l}constructor(t,e,a={}){super("group",t,e,a)}withCache(t=20){return super.withCache(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.cache||(e.cache=t)}),this}withRetry(t=2){return super.withRetry(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.retry||(e.retry=t)}),this}before(t){return super.before(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.before||(e.callbacks.before=t)}),this}after(t){return super.after(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.after||(e.callbacks.after=t)}),this}onCall(t){return super.onCall(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.call||(e.callbacks.call=t)}),this}}class _ extends ${constructor(t,e,a={},r="GET"){super("route",t,e,a),this.method=r,this.detectArguments()}static createRoute(t,e,a={},r){const i=new _(t,e,a,r);return s.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")}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace("[","").replace("]","");this.arguments.add(a)})}validate(t){return this.schema=t,this}}exports.Api=k;exports.Group=S;exports.Hook=A;exports.Klaim=C;exports.Registry=s;exports.Route=_;
1
+ "use strict";var L=Object.defineProperty;var G=(n,t,e)=>t in n?L(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var o=(n,t,e)=>G(n,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function T(n){return n.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}function M(n){return n.trim().replace(/^\/|\/$/g,"")}const O={limit:5,duration:10},$=new Map;function v(n,t){const e=Date.now(),a=t.duration*1e3;let r=$.get(n);r||(r={timestamps:[]},$.set(n,r));const i=r.timestamps.filter(s=>e-s<a);return i.length>=t.limit?!1:(i.push(e),r.timestamps=i,!0)}function N(n,t){const e=$.get(n);if(!e||e.timestamps.length<t.limit)return 0;const a=Date.now(),r=t.duration*1e3,l=[...e.timestamps].sort((u,f)=>u-f)[0]+r-a;return Math.max(0,l)}const j={page:1,pageParam:"page",limit:10,limitParam:"limit"};class k{constructor(t,e,a,r={}){o(this,"type");o(this,"name");o(this,"url");o(this,"headers");o(this,"parent");o(this,"method");o(this,"arguments",new Set);o(this,"schema");o(this,"pagination");o(this,"callbacks",{before:null,after:null,call:null});o(this,"cache",!1);o(this,"retry",!1);o(this,"rate",!1);o(this,"withCache",(t=20)=>(this.cache=t,this));o(this,"withRetry",(t=2)=>(this.retry=t,this));o(this,"withRate",(t={})=>(this.rate={...O,...t},this));this.type=t,this.name=T(e),this.name!==e&&console.warn(`Name "${e}" has been camelCased to "${this.name}"`),this.url=M(a),this.headers=r||{}}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}onCall(t){return this.callbacks.call=t,this}withPagination(t={}){return this.pagination={...j,...t},this}}const d=class d{constructor(){o(this,"cache");this.cache=new Map}static get i(){return d._instance||(d._instance=new d),d._instance}set(t,e,a=0){const r=Date.now()+a;this.cache.set(t,{data:e,expiry:r})}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}};o(d,"_instance");let P=d;function H(n){let a=2166136261;for(let i=0;i<n.length;i++)a^=n.charCodeAt(i),a*=16777619;let r=(a>>>0).toString(16).padStart(8,"0");for(;r.length<32;)a^=r.charCodeAt(r.length%r.length),a*=16777619,r+=(a>>>0).toString(16).padStart(8,"0");return r.substring(0,32)}async function W(n,t,e){const a=`${n.toString()}${JSON.stringify(t)}`,r=H(a);if(P.i.has(r))return P.i.get(r);const s=await(await fetch(n,t)).json();return P.i.set(r,s,e),s}class _{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}o(_,"_callbacks",new Map);const A={};function q(n,t){return async(...e)=>{if(t.pagination){const[i=0,s={},l={}]=e;return x(n,t,i,s,l)}const[a={},r={}]=e;return x(n,t,void 0,a,r)}}async function x(n,t,e,a={},r={}){const i=n.split(".");let s;for(let w=0;w<i.length;w++){const C=i[w];if(s=c.i.getApi(C),s)break}if(!t||!s||t.type!=="route"||s.type!=="api")throw new Error(`Invalid path: ${n}.${t.name}`);let l=z(`${s.url}/${t.url}`,t,a);if(t.pagination&&typeof e<"u"){const{pageParam:w="page",limit:C=10,limitParam:D="limit"}=t.pagination,E=new URLSearchParams;E.append(w,String(e)),E.append(D,String(C));const I=l.includes("?")?"&":"?";l=`${l}${I}${E.toString()}`}let u={};r&&t.method!=="GET"&&(u.body=JSON.stringify(r)),u.headers={"Content-Type":"application/json",...s.headers,...t.headers},u.method=t.method;const{beforeRoute:f,beforeApi:y,beforeUrl:h,beforeConfig:b}=B({route:t,api:s,url:l,config:u});l=h,u=b,c.updateElement(y),c.updateElement(f);let g=await V(s,t,l,u);t.schema&&"validate"in t.schema&&(g=await t.schema.validate(g));const{afterRoute:m,afterApi:K,afterData:U}=Q({route:t,api:s,response:g,data:g});return c.updateElement(K),c.updateElement(m),_.run(`${s.name}.${t.name}`),U}async function J(n,t,e,a){return n?await W(t,e,a.cache):await(await fetch(t,e)).json()}async function V(n,t,e,a){var f,y;const r=n.cache||t.cache,i=t.retry||n.retry||0;if(t.rate){const h=`${n.name}.${t.name}`;if(!v(h,t.rate)){const g=N(h,t.rate),m=Math.ceil(g/1e3);throw new Error(`Rate limit exceeded for ${h}. Try again in ${m} seconds.`)}}else if(n.rate){const h=`${n.name}`;if(!v(h,n.rate)){const g=N(h,n.rate),m=Math.ceil(g/1e3);throw new Error(`Rate limit exceeded for ${n.name} API. Try again in ${m} seconds.`)}}let s,l=!1,u=0;for(;u<=i&&!l;)try{(f=t.callbacks)!=null&&f.call?t.callbacks.call({}):(y=n.callbacks)!=null&&y.call&&n.callbacks.call({}),s=await J(!!r,e,a,n),l=!0}catch(h){if(u++,u>i)throw h.message=`Failed to fetch ${e} after ${i} attempts`,h}return s}function z(n,t,e){let a=n;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);a=a.replace(`[${r}]`,e[r])}),a}function B({route:n,api:t,url:e,config:a}){var i,s;const r=(s=(i=n.callbacks).before)==null?void 0:s.call(i,{route:n,api:t,url:e,config:a});return{beforeRoute:(r==null?void 0:r.route)||n,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||a}}function Q({route:n,api:t,response:e,data:a}){var i,s;const r=(s=(i=n.callbacks).after)==null?void 0:s.call(i,{route:n,api:t,response:e,data:a});return{afterRoute:(r==null?void 0:r.route)||n,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||a}}const p=class p{constructor(){o(this,"_elements",new Map);o(this,"_currentParent",null)}static get i(){return p._instance||(p._instance=new p),p._instance}registerElement(t){const e=this._currentParent;e&&(t.parent=this.getFullPath(e));const a=this.getElementKey(t);if(this._elements.set(a,t),t.type==="api"||t.type==="group"){let r=A;if(e){const i=this.getFullPath(e).split(".");for(const s of i)r[s]||(r[s]={}),r=r[s]}r[t.name]||(r[t.name]={})}}getCurrentParent(){return this._currentParent}setCurrentParent(t){const e=this._elements.get(t);if(!e||e.type!=="api"&&e.type!=="group")throw new Error(`Element ${t} not found or not a valid parent type`);this._currentParent=e}clearCurrentParent(){this._currentParent=null}registerRoute(t){if(!this._currentParent)throw new Error("No current parent set, use Route only inside Api or Group create callback");t.parent=this.getFullPath(this._currentParent);const e=this.getElementKey(t);this._elements.set(e,t),this.addToKlaimRoute(t)}addToKlaimRoute(t){if(!t.parent)return;let e=A;const a=t.parent.split(".");for(const r of a)e[r]||(e[r]={}),e=e[r];e[t.name]=q(t.parent,t)}getElementKey(t){return t?t.parent?`${t.parent}.${t.name}`:t.name:""}getFullPath(t){if(!t)return"";if(!t.parent)return t.name;const e=[t.name];let a=t;for(;a.parent;){const r=this._elements.get(a.parent);if(!r)break;e.unshift(r.name),a=r}return e.join(".")}getRoute(t,e){return this._elements.get(`${t}.${e}`)}getChildren(t){const e=[];return this._elements.forEach(a=>{a.parent===t&&e.push(a)}),e}static updateElement(t){return p.i._elements.get(p.i.getElementKey(t))||t}getApi(t){const e=this._elements.get(t);if(!e){for(const[a,r]of this._elements.entries())if(r.type==="api"&&a.endsWith(`.${t}`))return r;return}return e.type==="api"?e:this.findApi(e)}findApi(t){if(!t||!t.parent)return;const e=t.parent.split(".");for(let a=e.length;a>=0;a--){const r=e.slice(0,a).join("."),i=this._elements.get(r);if((i==null?void 0:i.type)==="api")return i}}};o(p,"_instance");let c=p;class S extends k{static create(t,e,a,r={}){const i=T(t);i!==t&&console.warn(`API name "${t}" has been camelCased to "${i}"`);const s=new S(i,e,r),l=c.i.getCurrentParent();c.i.registerElement(s);const u=l?c.i.getFullPath(l):"",f=u?`${u}.${i}`:i;return c.i.setCurrentParent(f),a(),l?c.i.setCurrentParent(c.i.getFullPath(l)):c.i.clearCurrentParent(),s}constructor(t,e,a={}){super("api",t,e,a)}}class F extends k{static create(t,e){const a=T(t),r=c.i.getCurrentParent(),i=r?c.i.getFullPath(r):"",s=i?`${i}.${a}`:a,l=new F(a,"");a!==t&&console.warn(`Group name "${t}" has been camelCased to "${a}"`),c.i.registerElement(l);const u=c.i.getCurrentParent();return c.i.setCurrentParent(s),e(),u?c.i.setCurrentParent(c.i.getFullPath(u)):c.i.clearCurrentParent(),l}constructor(t,e,a={}){super("group",t,e,a)}withCache(t=20){return super.withCache(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.cache||(e.cache=t)}),this}withRetry(t=2){return super.withRetry(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.retry||(e.retry=t)}),this}before(t){return super.before(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.before||(e.callbacks.before=t)}),this}after(t){return super.after(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.after||(e.callbacks.after=t)}),this}onCall(t){return super.onCall(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.call||(e.callbacks.call=t)}),this}}class R extends k{constructor(t,e,a={},r="GET"){super("route",t,e,a),this.method=r,this.detectArguments()}static createRoute(t,e,a={},r){const i=new R(t,e,a,r);return c.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")}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace("[","").replace("]","");this.arguments.add(a)})}validate(t){return this.schema=t,this}}exports.Api=S;exports.Group=F;exports.Hook=_;exports.Klaim=A;exports.Registry=c;exports.Route=R;
package/dist/klaim.es.js CHANGED
@@ -1,19 +1,40 @@
1
- var x = Object.defineProperty;
2
- var D = (n, t, e) => t in n ? x(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e;
3
- var o = (n, t, e) => D(n, typeof t != "symbol" ? t + "" : t, e);
4
- function b(n) {
1
+ var L = Object.defineProperty;
2
+ var G = (n, t, e) => t in n ? L(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e;
3
+ var o = (n, t, e) => G(n, typeof t != "symbol" ? t + "" : t, e);
4
+ function A(n) {
5
5
  return n.replace(/([-_][a-z])/gi, (t) => t.toUpperCase().replace("-", "").replace("_", "")).replace(/(^\w)/, (t) => t.toLowerCase());
6
6
  }
7
- function G(n) {
7
+ function M(n) {
8
8
  return n.trim().replace(/^\/|\/$/g, "");
9
9
  }
10
10
  const O = {
11
+ limit: 5,
12
+ duration: 10
13
+ // seconds
14
+ }, b = /* @__PURE__ */ new Map();
15
+ function k(n, t) {
16
+ const e = Date.now(), a = t.duration * 1e3;
17
+ let r = b.get(n);
18
+ r || (r = { timestamps: [] }, b.set(n, r));
19
+ const i = r.timestamps.filter(
20
+ (s) => e - s < a
21
+ );
22
+ return i.length >= t.limit ? !1 : (i.push(e), r.timestamps = i, !0);
23
+ }
24
+ function _(n, t) {
25
+ const e = b.get(n);
26
+ if (!e || e.timestamps.length < t.limit)
27
+ return 0;
28
+ const a = Date.now(), r = t.duration * 1e3, l = [...e.timestamps].sort((h, f) => h - f)[0] + r - a;
29
+ return Math.max(0, l);
30
+ }
31
+ const j = {
11
32
  page: 1,
12
33
  pageParam: "page",
13
34
  limit: 10,
14
35
  limitParam: "limit"
15
36
  };
16
- class E {
37
+ class T {
17
38
  /**
18
39
  * Creates a new element with the specified properties
19
40
  * @param {("api"|"route"|"group")} type - Element type identifier
@@ -38,6 +59,7 @@ class E {
38
59
  });
39
60
  o(this, "cache", !1);
40
61
  o(this, "retry", !1);
62
+ o(this, "rate", !1);
41
63
  /**
42
64
  * Enables response caching for this element
43
65
  * @param {number} [duration=20] - Cache duration in seconds
@@ -50,7 +72,20 @@ class E {
50
72
  * @returns {this} The element instance for chaining
51
73
  */
52
74
  o(this, "withRetry", (t = 2) => (this.retry = t, this));
53
- this.type = t, this.name = b(e), this.name !== e && console.warn(`Name "${e}" has been camelCased to "${this.name}"`), this.url = G(a), this.headers = r || {};
75
+ /**
76
+ * Enables rate limiting for this element
77
+ * @param {Partial<IRateLimitConfig>} [config] - Rate limiting configuration options
78
+ * @returns {this} The element instance for chaining
79
+ * @example
80
+ * ```typescript
81
+ * Route.get("getUser", "/users/[id]").withRate({ limit: 5, duration: 10 });
82
+ * ```
83
+ */
84
+ o(this, "withRate", (t = {}) => (this.rate = {
85
+ ...O,
86
+ ...t
87
+ }, this));
88
+ this.type = t, this.name = A(e), this.name !== e && console.warn(`Name "${e}" has been camelCased to "${this.name}"`), this.url = M(a), this.headers = r || {};
54
89
  }
55
90
  /**
56
91
  * Adds a before-request middleware callback
@@ -92,12 +127,12 @@ class E {
92
127
  */
93
128
  withPagination(t = {}) {
94
129
  return this.pagination = {
95
- ...O,
130
+ ...j,
96
131
  ...t
97
132
  }, this;
98
133
  }
99
134
  }
100
- const p = class p {
135
+ const d = class d {
101
136
  /**
102
137
  * Private constructor to enforce singleton pattern.
103
138
  * Initializes an empty cache storage.
@@ -125,7 +160,7 @@ const p = class p {
125
160
  * ```
126
161
  */
127
162
  static get i() {
128
- return p._instance || (p._instance = new p()), p._instance;
163
+ return d._instance || (d._instance = new d()), d._instance;
129
164
  }
130
165
  /**
131
166
  * Stores a value in the cache with an optional time-to-live duration.
@@ -188,9 +223,9 @@ const p = class p {
188
223
  *
189
224
  * @private
190
225
  */
191
- o(p, "_instance");
192
- let d = p;
193
- function I(n) {
226
+ o(d, "_instance");
227
+ let P = d;
228
+ function H(n) {
194
229
  let a = 2166136261;
195
230
  for (let i = 0; i < n.length; i++)
196
231
  a ^= n.charCodeAt(i), a *= 16777619;
@@ -199,14 +234,14 @@ function I(n) {
199
234
  a ^= r.charCodeAt(r.length % r.length), a *= 16777619, r += (a >>> 0).toString(16).padStart(8, "0");
200
235
  return r.substring(0, 32);
201
236
  }
202
- async function j(n, t, e) {
203
- const a = `${n.toString()}${JSON.stringify(t)}`, r = I(a);
204
- if (d.i.has(r))
205
- return d.i.get(r);
237
+ async function W(n, t, e) {
238
+ const a = `${n.toString()}${JSON.stringify(t)}`, r = H(a);
239
+ if (P.i.has(r))
240
+ return P.i.get(r);
206
241
  const s = await (await fetch(n, t)).json();
207
- return d.i.set(r, s, e), s;
242
+ return P.i.set(r, s, e), s;
208
243
  }
209
- class k {
244
+ class R {
210
245
  /**
211
246
  * Registers a callback function for a specific route.
212
247
  * If a callback already exists for the route, it will be replaced.
@@ -248,33 +283,33 @@ class k {
248
283
  *
249
284
  * @private
250
285
  */
251
- o(k, "_callbacks", /* @__PURE__ */ new Map());
252
- const $ = {};
253
- function H(n, t) {
286
+ o(R, "_callbacks", /* @__PURE__ */ new Map());
287
+ const F = {};
288
+ function q(n, t) {
254
289
  return async (...e) => {
255
290
  if (t.pagination) {
256
291
  const [i = 0, s = {}, l = {}] = e;
257
- return A(n, t, i, s, l);
292
+ return S(n, t, i, s, l);
258
293
  }
259
294
  const [a = {}, r = {}] = e;
260
- return A(n, t, void 0, a, r);
295
+ return S(n, t, void 0, a, r);
261
296
  };
262
297
  }
263
- async function A(n, t, e, a = {}, r = {}) {
298
+ async function S(n, t, e, a = {}, r = {}) {
264
299
  const i = n.split(".");
265
300
  let s;
266
- for (let g = 0; g < i.length; g++) {
267
- const y = i[g];
268
- if (s = c.i.getApi(y), s) break;
301
+ for (let w = 0; w < i.length; w++) {
302
+ const E = i[w];
303
+ if (s = c.i.getApi(E), s) break;
269
304
  }
270
305
  if (!t || !s || t.type !== "route" || s.type !== "api")
271
306
  throw new Error(`Invalid path: ${n}.${t.name}`);
272
- let l = W(`${s.url}/${t.url}`, t, a);
307
+ let l = z(`${s.url}/${t.url}`, t, a);
273
308
  if (t.pagination && typeof e < "u") {
274
- const { pageParam: g = "page", limit: y = 10, limitParam: K = "limit" } = t.pagination, C = new URLSearchParams();
275
- C.append(g, String(e)), C.append(K, String(y));
276
- const U = l.includes("?") ? "&" : "?";
277
- l = `${l}${U}${C.toString()}`;
309
+ const { pageParam: w = "page", limit: E = 10, limitParam: I = "limit" } = t.pagination, $ = new URLSearchParams();
310
+ $.append(w, String(e)), $.append(I, String(E));
311
+ const K = l.includes("?") ? "&" : "?";
312
+ l = `${l}${K}${$.toString()}`;
278
313
  }
279
314
  let h = {};
280
315
  r && t.method !== "GET" && (h.body = JSON.stringify(r)), h.headers = {
@@ -284,37 +319,50 @@ async function A(n, t, e, a = {}, r = {}) {
284
319
  }, h.method = t.method;
285
320
  const {
286
321
  beforeRoute: f,
287
- beforeApi: P,
288
- beforeUrl: m,
289
- beforeConfig: v
290
- } = J({ route: t, api: s, url: l, config: h });
291
- l = m, h = v, c.updateElement(P), c.updateElement(f);
292
- let w = await M(s, t, l, h);
293
- t.schema && "validate" in t.schema && (w = await t.schema.validate(w));
322
+ beforeApi: y,
323
+ beforeUrl: u,
324
+ beforeConfig: C
325
+ } = B({ route: t, api: s, url: l, config: h });
326
+ l = u, h = C, c.updateElement(y), c.updateElement(f);
327
+ let g = await V(s, t, l, h);
328
+ t.schema && "validate" in t.schema && (g = await t.schema.validate(g));
294
329
  const {
295
- afterRoute: T,
296
- afterApi: N,
297
- afterData: R
298
- } = z({ route: t, api: s, response: w, data: w });
299
- return c.updateElement(N), c.updateElement(T), k.run(`${s.name}.${t.name}`), R;
330
+ afterRoute: m,
331
+ afterApi: U,
332
+ afterData: D
333
+ } = Q({ route: t, api: s, response: g, data: g });
334
+ return c.updateElement(U), c.updateElement(m), R.run(`${s.name}.${t.name}`), D;
300
335
  }
301
- async function L(n, t, e, a) {
302
- return n ? await j(t, e, a.cache) : await (await fetch(t, e)).json();
336
+ async function J(n, t, e, a) {
337
+ return n ? await W(t, e, a.cache) : await (await fetch(t, e)).json();
303
338
  }
304
- async function M(n, t, e, a) {
305
- var f, P;
339
+ async function V(n, t, e, a) {
340
+ var f, y;
306
341
  const r = n.cache || t.cache, i = t.retry || n.retry || 0;
342
+ if (t.rate) {
343
+ const u = `${n.name}.${t.name}`;
344
+ if (!k(u, t.rate)) {
345
+ const g = _(u, t.rate), m = Math.ceil(g / 1e3);
346
+ throw new Error(`Rate limit exceeded for ${u}. Try again in ${m} seconds.`);
347
+ }
348
+ } else if (n.rate) {
349
+ const u = `${n.name}`;
350
+ if (!k(u, n.rate)) {
351
+ const g = _(u, n.rate), m = Math.ceil(g / 1e3);
352
+ throw new Error(`Rate limit exceeded for ${n.name} API. Try again in ${m} seconds.`);
353
+ }
354
+ }
307
355
  let s, l = !1, h = 0;
308
356
  for (; h <= i && !l; )
309
357
  try {
310
- (f = t.callbacks) != null && f.call ? t.callbacks.call({}) : (P = n.callbacks) != null && P.call && n.callbacks.call({}), s = await L(!!r, e, a, n), l = !0;
311
- } catch (m) {
358
+ (f = t.callbacks) != null && f.call ? t.callbacks.call({}) : (y = n.callbacks) != null && y.call && n.callbacks.call({}), s = await J(!!r, e, a, n), l = !0;
359
+ } catch (u) {
312
360
  if (h++, h > i)
313
- throw m.message = `Failed to fetch ${e} after ${i} attempts`, m;
361
+ throw u.message = `Failed to fetch ${e} after ${i} attempts`, u;
314
362
  }
315
363
  return s;
316
364
  }
317
- function W(n, t, e) {
365
+ function z(n, t, e) {
318
366
  let a = n;
319
367
  return t.arguments.forEach((r) => {
320
368
  if (e[r] === void 0)
@@ -322,7 +370,7 @@ function W(n, t, e) {
322
370
  a = a.replace(`[${r}]`, e[r]);
323
371
  }), a;
324
372
  }
325
- function J({ route: n, api: t, url: e, config: a }) {
373
+ function B({ route: n, api: t, url: e, config: a }) {
326
374
  var i, s;
327
375
  const r = (s = (i = n.callbacks).before) == null ? void 0 : s.call(i, { route: n, api: t, url: e, config: a });
328
376
  return {
@@ -332,7 +380,7 @@ function J({ route: n, api: t, url: e, config: a }) {
332
380
  beforeConfig: (r == null ? void 0 : r.config) || a
333
381
  };
334
382
  }
335
- function z({ route: n, api: t, response: e, data: a }) {
383
+ function Q({ route: n, api: t, response: e, data: a }) {
336
384
  var i, s;
337
385
  const r = (s = (i = n.callbacks).after) == null ? void 0 : s.call(i, { route: n, api: t, response: e, data: a });
338
386
  return {
@@ -342,7 +390,7 @@ function z({ route: n, api: t, response: e, data: a }) {
342
390
  afterData: (r == null ? void 0 : r.data) || a
343
391
  };
344
392
  }
345
- const u = class u {
393
+ const p = class p {
346
394
  constructor() {
347
395
  /**
348
396
  * Map storing all registered elements with their full paths as keys
@@ -360,7 +408,7 @@ const u = class u {
360
408
  * @returns The singleton Registry instance
361
409
  */
362
410
  static get i() {
363
- return u._instance || (u._instance = new u()), u._instance;
411
+ return p._instance || (p._instance = new p()), p._instance;
364
412
  }
365
413
  /**
366
414
  * Registers a new element in the registry and updates the Klaim object structure
@@ -377,7 +425,7 @@ const u = class u {
377
425
  e && (t.parent = this.getFullPath(e));
378
426
  const a = this.getElementKey(t);
379
427
  if (this._elements.set(a, t), t.type === "api" || t.type === "group") {
380
- let r = $;
428
+ let r = F;
381
429
  if (e) {
382
430
  const i = this.getFullPath(e).split(".");
383
431
  for (const s of i)
@@ -432,11 +480,11 @@ const u = class u {
432
480
  */
433
481
  addToKlaimRoute(t) {
434
482
  if (!t.parent) return;
435
- let e = $;
483
+ let e = F;
436
484
  const a = t.parent.split(".");
437
485
  for (const r of a)
438
486
  e[r] || (e[r] = {}), e = e[r];
439
- e[t.name] = H(t.parent, t);
487
+ e[t.name] = q(t.parent, t);
440
488
  }
441
489
  /**
442
490
  * Generates a unique key for an element based on its path
@@ -494,7 +542,7 @@ const u = class u {
494
542
  * @returns The updated element
495
543
  */
496
544
  static updateElement(t) {
497
- return u.i._elements.get(u.i.getElementKey(t)) || t;
545
+ return p.i._elements.get(p.i.getElementKey(t)) || t;
498
546
  }
499
547
  /**
500
548
  * Retrieves an API element by name, searching through the entire registry if necessary
@@ -528,9 +576,9 @@ const u = class u {
528
576
  }
529
577
  }
530
578
  };
531
- o(u, "_instance");
532
- let c = u;
533
- class _ extends E {
579
+ o(p, "_instance");
580
+ let c = p;
581
+ class v extends T {
534
582
  /**
535
583
  * Creates and registers a new API instance with the given configuration
536
584
  *
@@ -550,9 +598,9 @@ class _ extends E {
550
598
  * ```
551
599
  */
552
600
  static create(t, e, a, r = {}) {
553
- const i = b(t);
601
+ const i = A(t);
554
602
  i !== t && console.warn(`API name "${t}" has been camelCased to "${i}"`);
555
- const s = new _(i, e, r), l = c.i.getCurrentParent();
603
+ const s = new v(i, e, r), l = c.i.getCurrentParent();
556
604
  c.i.registerElement(s);
557
605
  const h = l ? c.i.getFullPath(l) : "", f = h ? `${h}.${i}` : i;
558
606
  return c.i.setCurrentParent(f), a(), l ? c.i.setCurrentParent(c.i.getFullPath(l)) : c.i.clearCurrentParent(), s;
@@ -570,7 +618,7 @@ class _ extends E {
570
618
  super("api", t, e, a);
571
619
  }
572
620
  }
573
- class F extends E {
621
+ class N extends T {
574
622
  /**
575
623
  * Creates a new group and registers it in the Registry.
576
624
  * Supports nested groups and inheritable configurations.
@@ -596,7 +644,7 @@ class F extends E {
596
644
  * ```
597
645
  */
598
646
  static create(t, e) {
599
- const a = b(t), r = c.i.getCurrentParent(), i = r ? c.i.getFullPath(r) : "", s = i ? `${i}.${a}` : a, l = new F(a, "");
647
+ const a = A(t), r = c.i.getCurrentParent(), i = r ? c.i.getFullPath(r) : "", s = i ? `${i}.${a}` : a, l = new N(a, "");
600
648
  a !== t && console.warn(`Group name "${t}" has been camelCased to "${a}"`), c.i.registerElement(l);
601
649
  const h = c.i.getCurrentParent();
602
650
  return c.i.setCurrentParent(s), e(), h ? c.i.setCurrentParent(c.i.getFullPath(h)) : c.i.clearCurrentParent(), l;
@@ -720,7 +768,7 @@ class F extends E {
720
768
  }), this;
721
769
  }
722
770
  }
723
- class S extends E {
771
+ class x extends T {
724
772
  /**
725
773
  * Creates a new Route instance.
726
774
  *
@@ -743,7 +791,7 @@ class S extends E {
743
791
  * @private
744
792
  */
745
793
  static createRoute(t, e, a = {}, r) {
746
- const i = new S(t, e, a, r);
794
+ const i = new x(t, e, a, r);
747
795
  return c.i.registerRoute(i), i;
748
796
  }
749
797
  /**
@@ -903,10 +951,10 @@ class S extends E {
903
951
  }
904
952
  }
905
953
  export {
906
- _ as Api,
907
- F as Group,
908
- k as Hook,
909
- $ as Klaim,
954
+ v as Api,
955
+ N as Group,
956
+ R as Hook,
957
+ F as Klaim,
910
958
  c as Registry,
911
- S as Route
959
+ x as Route
912
960
  };
package/dist/klaim.umd.js CHANGED
@@ -1 +1 @@
1
- (function(u,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(u=typeof globalThis<"u"?globalThis:u||self,p(u.klaim={}))})(this,function(u){"use strict";var z=Object.defineProperty;var B=(u,p,d)=>p in u?z(u,p,{enumerable:!0,configurable:!0,writable:!0,value:d}):u[p]=d;var o=(u,p,d)=>B(u,typeof p!="symbol"?p+"":p,d);function p(i){return i.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}function d(i){return i.trim().replace(/^\/|\/$/g,"")}const N={page:1,pageParam:"page",limit:10,limitParam:"limit"};class E{constructor(t,e,a,r={}){o(this,"type");o(this,"name");o(this,"url");o(this,"headers");o(this,"parent");o(this,"method");o(this,"arguments",new Set);o(this,"schema");o(this,"pagination");o(this,"callbacks",{before:null,after:null,call:null});o(this,"cache",!1);o(this,"retry",!1);o(this,"withCache",(t=20)=>(this.cache=t,this));o(this,"withRetry",(t=2)=>(this.retry=t,this));this.type=t,this.name=p(e),this.name!==e&&console.warn(`Name "${e}" has been camelCased to "${this.name}"`),this.url=d(a),this.headers=r||{}}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}onCall(t){return this.callbacks.call=t,this}withPagination(t={}){return this.pagination={...N,...t},this}}const g=class g{constructor(){o(this,"cache");this.cache=new Map}static get i(){return g._instance||(g._instance=new g),g._instance}set(t,e,a=0){const r=Date.now()+a;this.cache.set(t,{data:e,expiry:r})}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}};o(g,"_instance");let P=g;function R(i){let a=2166136261;for(let n=0;n<i.length;n++)a^=i.charCodeAt(n),a*=16777619;let r=(a>>>0).toString(16).padStart(8,"0");for(;r.length<32;)a^=r.charCodeAt(r.length%r.length),a*=16777619,r+=(a>>>0).toString(16).padStart(8,"0");return r.substring(0,32)}async function K(i,t,e){const a=`${i.toString()}${JSON.stringify(t)}`,r=R(a);if(P.i.has(r))return P.i.get(r);const c=await(await fetch(i,t)).json();return P.i.set(r,c,e),c}class ${static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}o($,"_callbacks",new Map);const A={};function G(i,t){return async(...e)=>{if(t.pagination){const[n=0,c={},l={}]=e;return v(i,t,n,c,l)}const[a={},r={}]=e;return v(i,t,void 0,a,r)}}async function v(i,t,e,a={},r={}){const n=i.split(".");let c;for(let y=0;y<n.length;y++){const F=n[y];if(c=s.i.getApi(F),c)break}if(!t||!c||t.type!=="route"||c.type!=="api")throw new Error(`Invalid path: ${i}.${t.name}`);let l=j(`${c.url}/${t.url}`,t,a);if(t.pagination&&typeof e<"u"){const{pageParam:y="page",limit:F=10,limitParam:W="limit"}=t.pagination,T=new URLSearchParams;T.append(y,String(e)),T.append(W,String(F));const J=l.includes("?")?"&":"?";l=`${l}${J}${T.toString()}`}let h={};r&&t.method!=="GET"&&(h.body=JSON.stringify(r)),h.headers={"Content-Type":"application/json",...c.headers,...t.headers},h.method=t.method;const{beforeRoute:m,beforeApi:w,beforeUrl:b,beforeConfig:I}=D({route:t,api:c,url:l,config:h});l=b,h=I,s.updateElement(w),s.updateElement(m);let C=await U(c,t,l,h);t.schema&&"validate"in t.schema&&(C=await t.schema.validate(C));const{afterRoute:L,afterApi:M,afterData:x}=H({route:t,api:c,response:C,data:C});return s.updateElement(M),s.updateElement(L),$.run(`${c.name}.${t.name}`),x}async function O(i,t,e,a){return i?await K(t,e,a.cache):await(await fetch(t,e)).json()}async function U(i,t,e,a){var m,w;const r=i.cache||t.cache,n=t.retry||i.retry||0;let c,l=!1,h=0;for(;h<=n&&!l;)try{(m=t.callbacks)!=null&&m.call?t.callbacks.call({}):(w=i.callbacks)!=null&&w.call&&i.callbacks.call({}),c=await O(!!r,e,a,i),l=!0}catch(b){if(h++,h>n)throw b.message=`Failed to fetch ${e} after ${n} attempts`,b}return c}function j(i,t,e){let a=i;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);a=a.replace(`[${r}]`,e[r])}),a}function D({route:i,api:t,url:e,config:a}){var n,c;const r=(c=(n=i.callbacks).before)==null?void 0:c.call(n,{route:i,api:t,url:e,config:a});return{beforeRoute:(r==null?void 0:r.route)||i,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||a}}function H({route:i,api:t,response:e,data:a}){var n,c;const r=(c=(n=i.callbacks).after)==null?void 0:c.call(n,{route:i,api:t,response:e,data:a});return{afterRoute:(r==null?void 0:r.route)||i,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||a}}const f=class f{constructor(){o(this,"_elements",new Map);o(this,"_currentParent",null)}static get i(){return f._instance||(f._instance=new f),f._instance}registerElement(t){const e=this._currentParent;e&&(t.parent=this.getFullPath(e));const a=this.getElementKey(t);if(this._elements.set(a,t),t.type==="api"||t.type==="group"){let r=A;if(e){const n=this.getFullPath(e).split(".");for(const c of n)r[c]||(r[c]={}),r=r[c]}r[t.name]||(r[t.name]={})}}getCurrentParent(){return this._currentParent}setCurrentParent(t){const e=this._elements.get(t);if(!e||e.type!=="api"&&e.type!=="group")throw new Error(`Element ${t} not found or not a valid parent type`);this._currentParent=e}clearCurrentParent(){this._currentParent=null}registerRoute(t){if(!this._currentParent)throw new Error("No current parent set, use Route only inside Api or Group create callback");t.parent=this.getFullPath(this._currentParent);const e=this.getElementKey(t);this._elements.set(e,t),this.addToKlaimRoute(t)}addToKlaimRoute(t){if(!t.parent)return;let e=A;const a=t.parent.split(".");for(const r of a)e[r]||(e[r]={}),e=e[r];e[t.name]=G(t.parent,t)}getElementKey(t){return t?t.parent?`${t.parent}.${t.name}`:t.name:""}getFullPath(t){if(!t)return"";if(!t.parent)return t.name;const e=[t.name];let a=t;for(;a.parent;){const r=this._elements.get(a.parent);if(!r)break;e.unshift(r.name),a=r}return e.join(".")}getRoute(t,e){return this._elements.get(`${t}.${e}`)}getChildren(t){const e=[];return this._elements.forEach(a=>{a.parent===t&&e.push(a)}),e}static updateElement(t){return f.i._elements.get(f.i.getElementKey(t))||t}getApi(t){const e=this._elements.get(t);if(!e){for(const[a,r]of this._elements.entries())if(r.type==="api"&&a.endsWith(`.${t}`))return r;return}return e.type==="api"?e:this.findApi(e)}findApi(t){if(!t||!t.parent)return;const e=t.parent.split(".");for(let a=e.length;a>=0;a--){const r=e.slice(0,a).join("."),n=this._elements.get(r);if((n==null?void 0:n.type)==="api")return n}}};o(f,"_instance");let s=f;class k extends E{static create(t,e,a,r={}){const n=p(t);n!==t&&console.warn(`API name "${t}" has been camelCased to "${n}"`);const c=new k(n,e,r),l=s.i.getCurrentParent();s.i.registerElement(c);const h=l?s.i.getFullPath(l):"",m=h?`${h}.${n}`:n;return s.i.setCurrentParent(m),a(),l?s.i.setCurrentParent(s.i.getFullPath(l)):s.i.clearCurrentParent(),c}constructor(t,e,a={}){super("api",t,e,a)}}class S extends E{static create(t,e){const a=p(t),r=s.i.getCurrentParent(),n=r?s.i.getFullPath(r):"",c=n?`${n}.${a}`:a,l=new S(a,"");a!==t&&console.warn(`Group name "${t}" has been camelCased to "${a}"`),s.i.registerElement(l);const h=s.i.getCurrentParent();return s.i.setCurrentParent(c),e(),h?s.i.setCurrentParent(s.i.getFullPath(h)):s.i.clearCurrentParent(),l}constructor(t,e,a={}){super("group",t,e,a)}withCache(t=20){return super.withCache(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.cache||(e.cache=t)}),this}withRetry(t=2){return super.withRetry(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.retry||(e.retry=t)}),this}before(t){return super.before(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.before||(e.callbacks.before=t)}),this}after(t){return super.after(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.after||(e.callbacks.after=t)}),this}onCall(t){return super.onCall(t),s.i.getChildren(s.i.getFullPath(this)).forEach(e=>{e.callbacks.call||(e.callbacks.call=t)}),this}}class _ extends E{constructor(t,e,a={},r="GET"){super("route",t,e,a),this.method=r,this.detectArguments()}static createRoute(t,e,a={},r){const n=new _(t,e,a,r);return s.i.registerRoute(n),n}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")}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace("[","").replace("]","");this.arguments.add(a)})}validate(t){return this.schema=t,this}}u.Api=k,u.Group=S,u.Hook=$,u.Klaim=A,u.Registry=s,u.Route=_,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})});
1
+ (function(h,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(h=typeof globalThis<"u"?globalThis:h||self,p(h.klaim={}))})(this,function(h){"use strict";var Q=Object.defineProperty;var X=(h,p,P)=>p in h?Q(h,p,{enumerable:!0,configurable:!0,writable:!0,value:P}):h[p]=P;var o=(h,p,P)=>X(h,typeof p!="symbol"?p+"":p,P);function p(n){return n.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}function P(n){return n.trim().replace(/^\/|\/$/g,"")}const I={limit:5,duration:10},$=new Map;function K(n,t){const e=Date.now(),a=t.duration*1e3;let r=$.get(n);r||(r={timestamps:[]},$.set(n,r));const i=r.timestamps.filter(s=>e-s<a);return i.length>=t.limit?!1:(i.push(e),r.timestamps=i,!0)}function U(n,t){const e=$.get(n);if(!e||e.timestamps.length<t.limit)return 0;const a=Date.now(),r=t.duration*1e3,l=[...e.timestamps].sort((u,d)=>u-d)[0]+r-a;return Math.max(0,l)}const L={page:1,pageParam:"page",limit:10,limitParam:"limit"};class A{constructor(t,e,a,r={}){o(this,"type");o(this,"name");o(this,"url");o(this,"headers");o(this,"parent");o(this,"method");o(this,"arguments",new Set);o(this,"schema");o(this,"pagination");o(this,"callbacks",{before:null,after:null,call:null});o(this,"cache",!1);o(this,"retry",!1);o(this,"rate",!1);o(this,"withCache",(t=20)=>(this.cache=t,this));o(this,"withRetry",(t=2)=>(this.retry=t,this));o(this,"withRate",(t={})=>(this.rate={...I,...t},this));this.type=t,this.name=p(e),this.name!==e&&console.warn(`Name "${e}" has been camelCased to "${this.name}"`),this.url=P(a),this.headers=r||{}}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}onCall(t){return this.callbacks.call=t,this}withPagination(t={}){return this.pagination={...L,...t},this}}const w=class w{constructor(){o(this,"cache");this.cache=new Map}static get i(){return w._instance||(w._instance=new w),w._instance}set(t,e,a=0){const r=Date.now()+a;this.cache.set(t,{data:e,expiry:r})}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}};o(w,"_instance");let y=w;function G(n){let a=2166136261;for(let i=0;i<n.length;i++)a^=n.charCodeAt(i),a*=16777619;let r=(a>>>0).toString(16).padStart(8,"0");for(;r.length<32;)a^=r.charCodeAt(r.length%r.length),a*=16777619,r+=(a>>>0).toString(16).padStart(8,"0");return r.substring(0,32)}async function M(n,t,e){const a=`${n.toString()}${JSON.stringify(t)}`,r=G(a);if(y.i.has(r))return y.i.get(r);const s=await(await fetch(n,t)).json();return y.i.set(r,s,e),s}class T{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}o(T,"_callbacks",new Map);const k={};function O(n,t){return async(...e)=>{if(t.pagination){const[i=0,s={},l={}]=e;return D(n,t,i,s,l)}const[a={},r={}]=e;return D(n,t,void 0,a,r)}}async function D(n,t,e,a={},r={}){const i=n.split(".");let s;for(let C=0;C<i.length;C++){const v=i[C];if(s=c.i.getApi(v),s)break}if(!t||!s||t.type!=="route"||s.type!=="api")throw new Error(`Invalid path: ${n}.${t.name}`);let l=H(`${s.url}/${t.url}`,t,a);if(t.pagination&&typeof e<"u"){const{pageParam:C="page",limit:v=10,limitParam:z="limit"}=t.pagination,N=new URLSearchParams;N.append(C,String(e)),N.append(z,String(v));const B=l.includes("?")?"&":"?";l=`${l}${B}${N.toString()}`}let u={};r&&t.method!=="GET"&&(u.body=JSON.stringify(r)),u.headers={"Content-Type":"application/json",...s.headers,...t.headers},u.method=t.method;const{beforeRoute:d,beforeApi:E,beforeUrl:f,beforeConfig:R}=W({route:t,api:s,url:l,config:u});l=f,u=R,c.updateElement(E),c.updateElement(d);let m=await j(s,t,l,u);t.schema&&"validate"in t.schema&&(m=await t.schema.validate(m));const{afterRoute:b,afterApi:J,afterData:V}=q({route:t,api:s,response:m,data:m});return c.updateElement(J),c.updateElement(b),T.run(`${s.name}.${t.name}`),V}async function x(n,t,e,a){return n?await M(t,e,a.cache):await(await fetch(t,e)).json()}async function j(n,t,e,a){var d,E;const r=n.cache||t.cache,i=t.retry||n.retry||0;if(t.rate){const f=`${n.name}.${t.name}`;if(!K(f,t.rate)){const m=U(f,t.rate),b=Math.ceil(m/1e3);throw new Error(`Rate limit exceeded for ${f}. Try again in ${b} seconds.`)}}else if(n.rate){const f=`${n.name}`;if(!K(f,n.rate)){const m=U(f,n.rate),b=Math.ceil(m/1e3);throw new Error(`Rate limit exceeded for ${n.name} API. Try again in ${b} seconds.`)}}let s,l=!1,u=0;for(;u<=i&&!l;)try{(d=t.callbacks)!=null&&d.call?t.callbacks.call({}):(E=n.callbacks)!=null&&E.call&&n.callbacks.call({}),s=await x(!!r,e,a,n),l=!0}catch(f){if(u++,u>i)throw f.message=`Failed to fetch ${e} after ${i} attempts`,f}return s}function H(n,t,e){let a=n;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);a=a.replace(`[${r}]`,e[r])}),a}function W({route:n,api:t,url:e,config:a}){var i,s;const r=(s=(i=n.callbacks).before)==null?void 0:s.call(i,{route:n,api:t,url:e,config:a});return{beforeRoute:(r==null?void 0:r.route)||n,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||a}}function q({route:n,api:t,response:e,data:a}){var i,s;const r=(s=(i=n.callbacks).after)==null?void 0:s.call(i,{route:n,api:t,response:e,data:a});return{afterRoute:(r==null?void 0:r.route)||n,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||a}}const g=class g{constructor(){o(this,"_elements",new Map);o(this,"_currentParent",null)}static get i(){return g._instance||(g._instance=new g),g._instance}registerElement(t){const e=this._currentParent;e&&(t.parent=this.getFullPath(e));const a=this.getElementKey(t);if(this._elements.set(a,t),t.type==="api"||t.type==="group"){let r=k;if(e){const i=this.getFullPath(e).split(".");for(const s of i)r[s]||(r[s]={}),r=r[s]}r[t.name]||(r[t.name]={})}}getCurrentParent(){return this._currentParent}setCurrentParent(t){const e=this._elements.get(t);if(!e||e.type!=="api"&&e.type!=="group")throw new Error(`Element ${t} not found or not a valid parent type`);this._currentParent=e}clearCurrentParent(){this._currentParent=null}registerRoute(t){if(!this._currentParent)throw new Error("No current parent set, use Route only inside Api or Group create callback");t.parent=this.getFullPath(this._currentParent);const e=this.getElementKey(t);this._elements.set(e,t),this.addToKlaimRoute(t)}addToKlaimRoute(t){if(!t.parent)return;let e=k;const a=t.parent.split(".");for(const r of a)e[r]||(e[r]={}),e=e[r];e[t.name]=O(t.parent,t)}getElementKey(t){return t?t.parent?`${t.parent}.${t.name}`:t.name:""}getFullPath(t){if(!t)return"";if(!t.parent)return t.name;const e=[t.name];let a=t;for(;a.parent;){const r=this._elements.get(a.parent);if(!r)break;e.unshift(r.name),a=r}return e.join(".")}getRoute(t,e){return this._elements.get(`${t}.${e}`)}getChildren(t){const e=[];return this._elements.forEach(a=>{a.parent===t&&e.push(a)}),e}static updateElement(t){return g.i._elements.get(g.i.getElementKey(t))||t}getApi(t){const e=this._elements.get(t);if(!e){for(const[a,r]of this._elements.entries())if(r.type==="api"&&a.endsWith(`.${t}`))return r;return}return e.type==="api"?e:this.findApi(e)}findApi(t){if(!t||!t.parent)return;const e=t.parent.split(".");for(let a=e.length;a>=0;a--){const r=e.slice(0,a).join("."),i=this._elements.get(r);if((i==null?void 0:i.type)==="api")return i}}};o(g,"_instance");let c=g;class _ extends A{static create(t,e,a,r={}){const i=p(t);i!==t&&console.warn(`API name "${t}" has been camelCased to "${i}"`);const s=new _(i,e,r),l=c.i.getCurrentParent();c.i.registerElement(s);const u=l?c.i.getFullPath(l):"",d=u?`${u}.${i}`:i;return c.i.setCurrentParent(d),a(),l?c.i.setCurrentParent(c.i.getFullPath(l)):c.i.clearCurrentParent(),s}constructor(t,e,a={}){super("api",t,e,a)}}class S extends A{static create(t,e){const a=p(t),r=c.i.getCurrentParent(),i=r?c.i.getFullPath(r):"",s=i?`${i}.${a}`:a,l=new S(a,"");a!==t&&console.warn(`Group name "${t}" has been camelCased to "${a}"`),c.i.registerElement(l);const u=c.i.getCurrentParent();return c.i.setCurrentParent(s),e(),u?c.i.setCurrentParent(c.i.getFullPath(u)):c.i.clearCurrentParent(),l}constructor(t,e,a={}){super("group",t,e,a)}withCache(t=20){return super.withCache(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.cache||(e.cache=t)}),this}withRetry(t=2){return super.withRetry(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.retry||(e.retry=t)}),this}before(t){return super.before(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.before||(e.callbacks.before=t)}),this}after(t){return super.after(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.after||(e.callbacks.after=t)}),this}onCall(t){return super.onCall(t),c.i.getChildren(c.i.getFullPath(this)).forEach(e=>{e.callbacks.call||(e.callbacks.call=t)}),this}}class F extends A{constructor(t,e,a={},r="GET"){super("route",t,e,a),this.method=r,this.detectArguments()}static createRoute(t,e,a={},r){const i=new F(t,e,a,r);return c.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")}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace("[","").replace("]","");this.arguments.add(a)})}validate(t){return this.schema=t,this}}h.Api=_,h.Group=S,h.Hook=T,h.Klaim=k,h.Registry=c,h.Route=F,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaim",
3
- "version": "1.8.87",
3
+ "version": "1.9.1",
4
4
  "author": "antharuu",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,15 +9,15 @@
9
9
  "main": "dist/klaim.cjs.js",
10
10
  "module": "dist/klaim.es.js",
11
11
  "devDependencies": {
12
- "@eslint/js": "9.25.1",
12
+ "@eslint/js": "9.26.0",
13
13
  "@stylistic/eslint-plugin": "4.2.0",
14
- "@types/bun": "latest",
14
+ "@types/bun": "^1.2.11",
15
15
  "@types/eslint__js": "9.14.0",
16
16
  "@types/node": "^22.15.3",
17
17
  "@vitest/coverage-v8": "^3.1.2",
18
18
  "@vitest/ui": "^3.1.2",
19
19
  "dotenv-cli": "^8.0.0",
20
- "eslint": "9.25.1",
20
+ "eslint": "9.26.0",
21
21
  "eslint-plugin-jsdoc": "50.6.11",
22
22
  "eslint-plugin-simple-import-sort": "12.1.1",
23
23
  "jsdom": "^26.1.0",
@@ -1,5 +1,6 @@
1
1
  import cleanUrl from "../tools/cleanUrl";
2
2
  import toCamelCase from "../tools/toCamelCase";
3
+ import { IRateLimitConfig, DEFAULT_RATE_LIMIT_CONFIG } from "../tools/rateLimit";
3
4
 
4
5
  /**
5
6
  * Type definition for HTTP headers
@@ -96,6 +97,8 @@ export interface IElement {
96
97
  cache: false | number;
97
98
  /** Number of retry attempts, or false if retries are disabled */
98
99
  retry: false | number;
100
+ /** Rate limiting configuration, or false if rate limiting is disabled */
101
+ rate: false | IRateLimitConfig;
99
102
  /** Reference to parent element name */
100
103
  parent?: string;
101
104
  /** HTTP method for routes */
@@ -124,6 +127,9 @@ export interface IElement {
124
127
 
125
128
  /** Configures pagination settings */
126
129
  withPagination(config?: IPaginationConfig): this;
130
+
131
+ /** Enables rate limiting */
132
+ withRate(config?: Partial<IRateLimitConfig>): this;
127
133
  }
128
134
 
129
135
  /**
@@ -162,6 +168,7 @@ export abstract class Element implements IElement {
162
168
 
163
169
  public cache: false | number = false;
164
170
  public retry: false | number = false;
171
+ public rate: false | IRateLimitConfig = false;
165
172
 
166
173
  /**
167
174
  * Creates a new element with the specified properties
@@ -257,4 +264,21 @@ export abstract class Element implements IElement {
257
264
  };
258
265
  return this;
259
266
  }
267
+
268
+ /**
269
+ * Enables rate limiting for this element
270
+ * @param {Partial<IRateLimitConfig>} [config] - Rate limiting configuration options
271
+ * @returns {this} The element instance for chaining
272
+ * @example
273
+ * ```typescript
274
+ * Route.get("getUser", "/users/[id]").withRate({ limit: 5, duration: 10 });
275
+ * ```
276
+ */
277
+ public withRate = (config: Partial<IRateLimitConfig> = {}): this => {
278
+ this.rate = {
279
+ ...DEFAULT_RATE_LIMIT_CONFIG,
280
+ ...config
281
+ };
282
+ return this;
283
+ };
260
284
  }
package/src/core/Klaim.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import fetchWithCache from "../tools/fetchWithCache";
2
+ import { checkRateLimit, getTimeUntilNextRequest } from "../tools/rateLimit";
2
3
  import {IElement} from "./Element";
3
4
  import {Hook} from "./Hook";
4
5
  import {Registry} from "./Registry";
@@ -41,10 +42,7 @@ export type IApiReference = Record<string, IRouteReference>;
41
42
  * await Klaim.apiName.routeName();
42
43
  *
43
44
  * // With pagination
44
- * await Klaim.apiName.routeName(2, 10); // Page 2, 10 items per page
45
- *
46
- * // With pagination and additional parameters
47
- * await Klaim.apiName.routeName(2, 10, { filter: 'active' });
45
+ * await Klaim.apiName.routeName(2); // Page 2
48
46
  * ```
49
47
  */
50
48
  export const Klaim: IApiReference = {};
@@ -191,14 +189,14 @@ async function fetchData(
191
189
  }
192
190
 
193
191
  /**
194
- * Performs a fetch request with retry capability
192
+ * Performs a fetch request with retry capability and rate limiting
195
193
  *
196
194
  * @param api - API element containing retry settings
197
195
  * @param route - Route element containing retry settings
198
196
  * @param url - The URL to fetch from
199
197
  * @param config - Fetch configuration options
200
198
  * @returns Promise resolving to the parsed response
201
- * @throws Error after all retry attempts fail
199
+ * @throws Error after all retry attempts fail or if rate limited
202
200
  */
203
201
  async function fetchWithRetry(
204
202
  api: IElement,
@@ -208,6 +206,31 @@ async function fetchWithRetry(
208
206
  ): Promise<any> {
209
207
  const withCache = api.cache || route.cache;
210
208
  const maxRetries = (route.retry || api.retry) || 0;
209
+
210
+ // Check rate limiting
211
+ // Si la route a sa propre configuration de limite, on l'utilise avec une clé spécifique à la route
212
+ if (route.rate) {
213
+ const routeKey = `${api.name}.${route.name}`;
214
+ const allowed = checkRateLimit(routeKey, route.rate);
215
+
216
+ if (!allowed) {
217
+ const waitTime = getTimeUntilNextRequest(routeKey, route.rate);
218
+ const waitSeconds = Math.ceil(waitTime / 1000);
219
+ throw new Error(`Rate limit exceeded for ${routeKey}. Try again in ${waitSeconds} seconds.`);
220
+ }
221
+ }
222
+ // Si l'API a une configuration de limite et que la route n'en a pas, utiliser une clé au niveau de l'API
223
+ else if (api.rate) {
224
+ const apiKey = `${api.name}`;
225
+ const allowed = checkRateLimit(apiKey, api.rate);
226
+
227
+ if (!allowed) {
228
+ const waitTime = getTimeUntilNextRequest(apiKey, api.rate);
229
+ const waitSeconds = Math.ceil(waitTime / 1000);
230
+ throw new Error(`Rate limit exceeded for ${api.name} API. Try again in ${waitSeconds} seconds.`);
231
+ }
232
+ }
233
+
211
234
  let response;
212
235
  let success = false;
213
236
  let attempt = 0;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Configuration interface for rate limiting settings
3
+ * @interface IRateLimitConfig
4
+ * @property {number} limit - Maximum number of requests allowed in the given duration
5
+ * @property {number} duration - Time window in seconds for the rate limit
6
+ */
7
+ export interface IRateLimitConfig {
8
+ limit: number;
9
+ duration: number;
10
+ }
11
+
12
+ /**
13
+ * Default configuration for rate limiting
14
+ * @constant DEFAULT_RATE_LIMIT_CONFIG
15
+ */
16
+ export const DEFAULT_RATE_LIMIT_CONFIG: IRateLimitConfig = {
17
+ limit: 5,
18
+ duration: 10 // seconds
19
+ };
20
+
21
+ // Store request timestamps for rate limiting
22
+ type RequestLog = {
23
+ timestamps: number[]; // Timestamps of requests
24
+ };
25
+
26
+ // Global request log
27
+ const requestLogs: Map<string, RequestLog> = new Map();
28
+
29
+ /**
30
+ * Checks if a request should be rate limited
31
+ *
32
+ * @param key - Unique identifier for the API/route combination
33
+ * @param config - Rate limiting configuration
34
+ * @returns True if the request should be allowed, false if it should be rate limited
35
+ */
36
+ export function checkRateLimit(key: string, config: IRateLimitConfig): boolean {
37
+ const now = Date.now();
38
+ const timeWindow = config.duration * 1000; // Convert to milliseconds
39
+
40
+ // Get or initialize request log for this key
41
+ let requestLog = requestLogs.get(key);
42
+ if (!requestLog) {
43
+ requestLog = { timestamps: [] };
44
+ requestLogs.set(key, requestLog);
45
+ }
46
+
47
+ // Clean up old timestamps outside the current time window
48
+ const validTimestamps = requestLog.timestamps.filter(
49
+ time => now - time < timeWindow
50
+ );
51
+
52
+ // Check if we've hit the rate limit
53
+ if (validTimestamps.length >= config.limit) {
54
+ return false; // Rate limited
55
+ }
56
+
57
+ // Add current timestamp and update the log
58
+ validTimestamps.push(now);
59
+ requestLog.timestamps = validTimestamps;
60
+
61
+ return true; // Request allowed
62
+ }
63
+
64
+ /**
65
+ * Calculates time remaining until the next request is allowed
66
+ *
67
+ * @param key - Unique identifier for the API/route combination
68
+ * @param config - Rate limiting configuration
69
+ * @returns Time in milliseconds until the next request is allowed, or 0 if not rate limited
70
+ */
71
+ export function getTimeUntilNextRequest(key: string, config: IRateLimitConfig): number {
72
+ const requestLog = requestLogs.get(key);
73
+ if (!requestLog || requestLog.timestamps.length < config.limit) {
74
+ return 0; // Not rate limited
75
+ }
76
+
77
+ const now = Date.now();
78
+ const timeWindow = config.duration * 1000;
79
+
80
+ // Sort timestamps in ascending order
81
+ const sortedTimestamps = [...requestLog.timestamps].sort((a, b) => a - b);
82
+
83
+ // Oldest timestamp + timeWindow = when it will expire
84
+ const oldestValidTime = sortedTimestamps[0];
85
+ const timeUntilExpiry = (oldestValidTime + timeWindow) - now;
86
+
87
+ return Math.max(0, timeUntilExpiry);
88
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { Api, Klaim, Route } from "../src";
3
+
4
+ // Mock fetch pour simuler les réponses API sans faire de vraies requêtes
5
+ global.fetch = vi.fn(() =>
6
+ Promise.resolve({
7
+ json: () => Promise.resolve({ success: true }),
8
+ })
9
+ ) as any;
10
+
11
+ // Réinitialiser les mocks entre chaque test
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe("Rate Limiting", () => {
17
+ it("should allow requests within the rate limit", async () => {
18
+ const apiName = "testRateApi";
19
+ const apiUrl = "https://example.com";
20
+ const routeName = "testRateRoute";
21
+
22
+ Api.create(apiName, apiUrl, () => {
23
+ Route.get(routeName, "/test").withRate({ limit: 3, duration: 10 });
24
+ });
25
+
26
+ // Devrait permettre 3 requêtes successives sans problème
27
+ await Klaim[apiName][routeName]();
28
+ await Klaim[apiName][routeName]();
29
+ await Klaim[apiName][routeName]();
30
+
31
+ // Vérifier que fetch a été appelé 3 fois
32
+ expect(fetch).toHaveBeenCalledTimes(3);
33
+ });
34
+
35
+ it("should block requests that exceed the rate limit", async () => {
36
+ const apiName = "testRateApi2";
37
+ const apiUrl = "https://example.com";
38
+ const routeName = "testRateRoute2";
39
+
40
+ Api.create(apiName, apiUrl, () => {
41
+ Route.get(routeName, "/test").withRate({ limit: 2, duration: 10 });
42
+ });
43
+
44
+ // Les deux premières requêtes devraient réussir
45
+ await Klaim[apiName][routeName]();
46
+ await Klaim[apiName][routeName]();
47
+
48
+ // La troisième requête devrait être bloquée
49
+ await expect(Klaim[apiName][routeName]()).rejects.toThrow(/Rate limit exceeded/);
50
+
51
+ // Vérifier que fetch n'a été appelé que 2 fois
52
+ expect(fetch).toHaveBeenCalledTimes(2);
53
+ });
54
+
55
+ it("should apply rate limits at the API level", async () => {
56
+ const apiName = "testRateApi3";
57
+ const apiUrl = "https://example.com";
58
+ const routeName1 = "testRateRoute3A";
59
+ const routeName2 = "testRateRoute3B";
60
+
61
+ Api.create(apiName, apiUrl, () => {
62
+ Route.get(routeName1, "/test1");
63
+ Route.get(routeName2, "/test2");
64
+ }).withRate({ limit: 3, duration: 10 });
65
+
66
+ // Différentes routes mais même API - devrait compter pour la même limite
67
+ await Klaim[apiName][routeName1]();
68
+ await Klaim[apiName][routeName1]();
69
+ await Klaim[apiName][routeName2]();
70
+
71
+ // La quatrième requête devrait être bloquée
72
+ await expect(Klaim[apiName][routeName1]()).rejects.toThrow(/Rate limit exceeded/);
73
+
74
+ // Vérifier que fetch n'a été appelé que 3 fois
75
+ expect(fetch).toHaveBeenCalledTimes(3);
76
+ });
77
+
78
+ it("should respect route-specific rate limits over API-level limits", async () => {
79
+ const apiName = "testRateApi4";
80
+ const apiUrl = "https://example.com";
81
+ const routeName1 = "testRateRoute4A";
82
+ const routeName2 = "testRateRoute4B";
83
+
84
+ Api.create(apiName, apiUrl, () => {
85
+ Route.get(routeName1, "/test1").withRate({ limit: 1, duration: 10 }); // Limite plus stricte
86
+ Route.get(routeName2, "/test2"); // Utilise la limite de l'API
87
+ }).withRate({ limit: 5, duration: 10 });
88
+
89
+ // La première route a une limite de 1
90
+ await Klaim[apiName][routeName1]();
91
+ await expect(Klaim[apiName][routeName1]()).rejects.toThrow(/Rate limit exceeded/);
92
+
93
+ // La deuxième route utilise la limite de l'API (5)
94
+ await Klaim[apiName][routeName2]();
95
+ await Klaim[apiName][routeName2]();
96
+ await Klaim[apiName][routeName2]();
97
+ await Klaim[apiName][routeName2]();
98
+ await Klaim[apiName][routeName2]();
99
+ await expect(Klaim[apiName][routeName2]()).rejects.toThrow(/Rate limit exceeded/);
100
+
101
+ // Vérifier que fetch a été appelé 6 fois (1 + 5)
102
+ expect(fetch).toHaveBeenCalledTimes(6);
103
+ });
104
+ });