@jointly/cache-candidate 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,130 +1,202 @@
1
1
  # What is it?
2
2
 
3
- This is a library providing both a decorator and a higher-order function to cache the result of a method if given conditions are met.
3
+ This is a library providing both a higher-order function and a decorator to cache the result of a function/method if given conditions are met.
4
4
 
5
- # How does it work?
5
+ # Examples
6
6
 
7
- ## Decorator
7
+ ## Use-case #1: DB query
8
8
 
9
- The decorator expects to receive an object with a partial of the properties contained in the `CacheCandidateOptions` interface.
10
- Every non-passed property will be set to its default value using the `CacheCandidateOptionsDefault` object.
11
- The decorator will return a method that will return a Promise fulfilled with the cached value if the method has already been called with the same arguments and the conditions are met.
12
- The conditions are, within the given `timeFrame`:
9
+ In this scenario, we want to cache the result of the function if the same parameters are passed 3 times in the last 30 seconds, but we want to keep the cache record for 60 seconds.
13
10
 
14
- - If a `candidateFunction` is provided, it returns `true` for at least `requestThreshold` times.
15
- - If a `millisecondsThreshold` is provided, it passed such threshold (Execution time) at least `requestThreshold` times.
16
- - If only a `requestThreshold` is provided (default), it is called at least `requestThreshold` times.
11
+ ```js
12
+ import { cacheCandidate } from '@jointly/cache-candidate';
17
13
 
18
- You can pass an additional `dependencyKeys` property to the decorator options which provides an invalidation mechanism to be called manually in your codebase.
19
- This property can be either an array of string, a function that returns an array of string or a function that returns a Promise fulfilled with an array of string.
20
- Both the function and the Promise will receive the result of the method on which the CacheCandidate operates.
21
- In case of an async method, the promise will be fulfilled before passing the result to the `dependencyKeys` function.
22
- The `dependencyKeys` function will be called only if the cache adapter correctly sets the value in the cache (i.e. the `.set` method is fulfilled).
14
+ function getUsers(filters = {}) {
15
+ return db.query('SELECT * FROM users WHERE ?', filters);
16
+ }
23
17
 
24
- ## Higher-order function
18
+ const cachedGetUsers = cacheCandidate(getUsers, {
19
+ requestsThreshold: 3,
20
+ ttl: 60000,
21
+ timeFrame: 30000
22
+ });
23
+
24
+ await cachedGetUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3 in the last 30 seconds
25
+ await cachedGetUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3 in the last 30 seconds
26
+ await cachedGetUsers({ name: 'John' }); // <-- This WILL be cached, because the requestsThreshold is 3 in the last 30 seconds!
27
+ await cachedGetUsers({ name: 'Jack' }); // <-- This won't be cached, because parameters are different
28
+ await sleep(60000); // <-- This will flush the cache because of the ttl
29
+ await cachedGetUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3 in the last 30 seconds
30
+ ```
31
+ ## Use-case #2: DB query - Different timeFrame
25
32
 
26
- Everything is the same as the decorator, but the function takes the original function as the first argument and the options as the second argument.
27
- The function returns a new function that will return a Promise fulfilled with the cached value if the method has already been called with the same arguments and the conditions are met.
33
+ In this scenario, we want to cache the result of the function if the same parameters are passed 3 times in the last 45 seconds, but we want to keep the cache record for 30 seconds. This example could reflect a scenario in which you are paying for the cache storage, yet you want to cache the same result again if the same parameters are passed 3 times in a short period of time.
34
+ ```js
35
+ import { cacheCandidate } from '@jointly/cache-candidate';
28
36
 
29
- ## Key composition
37
+ function getUsers(filters = {}) {
38
+ return db.query('SELECT * FROM users WHERE ?', filters);
39
+ }
30
40
 
31
- The cache key is composed based on the following criteria:
41
+ const cachedGetUsers = cacheCandidate(getUsers, {
42
+ requestsThreshold: 3,
43
+ ttl: 30000,
44
+ timeFrame: 45000 // <-- Notice the different timeFrame, higher than cache ttl
45
+ });
46
+
47
+ await cachedGetUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3 in the last 45 seconds
48
+ await cachedGetUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3 in the last 45 seconds
49
+ await cachedGetUsers({ name: 'John' }); // <-- This WILL be cached, because the requestsThreshold is 3 in the last 45 seconds!
50
+ await cachedGetUsers({ name: 'Jack' }); // <-- This won't be cached, because parameters are different
51
+ await sleep(30000); // <-- This will flush the cache because of the ttl
52
+ await cachedGetUsers({ name: 'John' }); // <-- This WILL be cached, because the requestsThreshold is 3 in the last 45 seconds!
53
+ ```
32
54
 
33
- ### Decorator
55
+ ## Use-case #3: Advanced Usage - Candidate Function
34
56
 
35
- - The class constructor name.
36
- - The method name.
37
- - The arguments passed to the method. (JSON.stringify)
38
- - `instanceIdentifier`: A uniqid generated for each instance of the class. It uses the instance properties to generate the id. (JSON.stringify)
39
- - `uniqueIdentifier`: A uniqid generated to allow multiple files to contain the same class with the same method.
57
+ You can also pass a candidate function to check if the conditions are met.
40
58
 
59
+ ```js
60
+ import { cacheCandidate } from '@jointly/cache-candidate';
41
61
 
42
- ### Higher-order function
62
+ function getUsers(filters = {}) {
63
+ return db.query('SELECT * FROM users WHERE ?', filters);
64
+ }
43
65
 
44
- - The arguments passed to the method. (JSON.stringify)
45
- - `uniqueIdentifier`: A uniqid generated to allow multiple files to contain the same class with the same method.
66
+ const cachedGetUsers = cacheCandidate(getUsers, {
67
+ ttl: 30000,
68
+ candidateFunction: ({ timeFrameCacheRecords, options, args }) => args[0].name === 'John',
69
+ });
70
+
71
+ await cachedGetUsers({ name: 'John' }); // <-- This will be cached, because the candidateFunction returns true
72
+ await cachedGetUsers({ name: 'John' }); // <-- This will return the cached value
73
+ await cachedGetUsers({ name: 'Jack' }); // <-- This won't be cached, because the candidateFunction returns false
74
+ await sleep(30000); // <-- This will invalidate the cache because of the ttl
75
+ ```
76
+
77
+ # How does it work?
46
78
 
79
+ ## Higher-order function
80
+
81
+ The library exposes the `cacheCandidate` function which accepts the function to be cached as the first argument and the options as the second argument.
82
+ The returned function is an async function which returns a Promise fulfilled with the cached value if the method has already been called with the same arguments and/or the conditions are met.
83
+
84
+ The options available are:
85
+ - `ttl` (_optional_): The time to live of the cache record in milliseconds. Default: `600000` (10 minutes).
86
+ - `timeFrame` (_optional_): The timeframe considered for the condition checks. Default: `30000` (30 seconds).
87
+ Consider the timeFrame as `the execution history of the last X milliseconds`. This timeframe collects information about the function's execution time.
88
+ For example, if you set the timeFrame to 30000 (30 seconds), the library will check the execution history of the last 30 seconds.
89
+ This means that if you set the `requestsThreshold` (explained below) to 3, the function will be cached only if the same parameters are passed 3 times in the last 30 seconds.
90
+ - `candidateFunction` (_optional_): The function to be called to check if the conditions are met. If not passed, this criteria will be ignored.
91
+ The candidateFunction receives an object with the following properties:
92
+ - `options`: The options passed to the `cacheCandidate` function.
93
+ - `executionTime`: The execution time of the current function execution in milliseconds.
94
+ - `args`: The arguments passed to the current function.
95
+ - `timeFrameCacheRecords`: The cache records of the last `timeFrame` milliseconds.
96
+ - `millisecondThreshold` (_optional_): The threshold in milliseconds to be considered for the condition checks. If not passed, this criteria will be ignored.
97
+ - `requestsThreshold` (_optional_): The number of requests to be considered for the condition checks. Default: `3`.
98
+ - `keepAlive` (_optional_): If `true`, the cache record will be kept alive at every request. Default: `false`.
99
+ - `cache` (_optional_): The cache adapter to be used. Defaults to `an in-memory cache based on Maps, but with Promises`.
100
+ Available adapters are:
101
+ - `makeRedisCache`: A cache adapter based on Redis. Receives a Redis client as the first and only argument.
102
+ - `events` (_optional_): Listener functions to be called at specific steps of the process.
103
+ Available events are:
104
+ - `onCacheHit`: Called when the cache entry is hit.
105
+ - `onCacheSet`: Called when the cache entry is set.
106
+ - `onCacheDelete`: Called when the cache entry is deleted.
107
+ - `onBeforeFunctionExecution`: Called before the function execution.
108
+ - `onAfterFunctionExecution`: Called after the function execution.
109
+ Every event receives an object containing the `key` property, which is the key used for the cache.
110
+ The `onAfterFunctionExecution` event also receives the `executionTime` property, which is the execution time of the current function in milliseconds.
111
+ - `plugins` (_optional_): An array of plugins to be used. Default: `[]`.
112
+ Please, refer to the [@jointly/cache-candidate-plugin-base](https://github.com/JointlyTech/cache-candidate-plugin-base) package for more information.
47
113
 
48
- ## Cache invalidation
114
+ ## Decorator
49
115
 
50
- The cache invalidation is done using the exported `cacheCandidateDependencyManager` object.
51
- The object exposes the `invalidate` method which accepts a string.
52
- The string is one of the dependency keys returned by the `dependencyKeys` function/array defined in the decorator options.
116
+ The decorator expects to receive the options as the first argument and works exactly as the higher-order function.
53
117
 
54
118
  ### Example
55
119
 
56
- ```typescript
57
- import { cacheCandidate, cacheCandidateDependencyManager } from 'cache-candidate';
120
+ ```ts
121
+ import { CacheCandidate } from '@jointly/cache-candidate';
58
122
 
59
123
  class MyClass {
60
124
  @CacheCandidate({
61
- dependencyKeys: (users) => {
62
- return users.map((user) => `users-${user.id}`);
63
- },
125
+ requestsThreshold: 3,
126
+ ttl: 30000,
64
127
  })
65
- public async getUsers() {
66
- // Do something
67
- return users;
68
- }
69
-
70
- public async updateUser(user) {
71
- // Do something
72
- cacheCandidateDependencyManager.invalidate(`users-${user.id}`);
128
+ async getUsers(filters = {}) {
129
+ return db.query('SELECT * FROM users WHERE ?', filters);
73
130
  }
74
131
  }
75
132
 
76
- const myClass = new MyClass();
77
- const users = await myClass.getUsers();
78
- users[0].name = 'New name';
79
- await myClass.updateUser(users[0]); // This will invalidate the cache
133
+ const myInstance = new MyClass();
134
+ await myInstance.getUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3
135
+ await myInstance.getUsers({ name: 'John' }); // <-- This won't be cached, because the requestsThreshold is 3
136
+ await myInstance.getUsers({ name: 'John' }); // <-- This WILL be cached, because the requestsThreshold is 3!
80
137
  ```
81
138
 
139
+ ## Conditions / Criterias
140
+ The conditions are, within the given `timeFrame`:
141
+
142
+ - If a `candidateFunction` is provided, it returns `true` at least once.
143
+ The candidateFunction ignores all the other conditions.
144
+ - If a `millisecondsThreshold` is provided, the function execution time passed such threshold at least `requestThreshold` times.
145
+ - If only a `requestThreshold` is provided (default), the function is called at least `requestThreshold` times.
82
146
 
83
147
  # Other Info
84
148
 
149
+ ## Plugins
150
+
151
+ The library supports plugins to extend its functionality.
152
+ Please, refer to the [@jointly/cache-candidate-plugin-base](https://github.com/JointlyTech/cache-candidate-plugin-base) package for the documentation on how to create a plugin.
153
+
154
+ ### First-party plugins
155
+
156
+ - [@jointly/cache-candidate-plugin-dependency-keys](https://github.com/JointlyTech/cache-candidate-plugin-dependency-keys): A plugin allowing to define dependency keys when setting the cache record.
157
+ This provides a mechanism to delete one or more cache records when a dependency key is invalidated.
158
+
85
159
  ## Constraints
86
160
 
87
- - The decorator can only be applied to methods that return a `Promise`.
88
- - The `candidateFunction` must be synchronous to maintain good performances. If passed an async function bypassing type checking, the candidateFunction will return a Promise thus not working properly.
161
+ - The higher-order function and the decorator only work with async functions.
162
+ Please, refer to the [Considerations on synchronous functions](#considerations-on-synchronous-functions) section for more information.
163
+ - The `candidateFunction` must be synchronous. If passed an async function bypassing type checking, the candidateFunction will return a Promise thus not working properly. This choice was made to avoid the overhead and the performance burden of an async function call.
89
164
 
90
165
  ## Cache Stampede
91
166
 
92
- The decorator prevents the cache stampede problem by using a `Map` called `runningQueries` which saves the promise of the method call.
93
- If multiple calls are made to the same method with the same arguments, the method will be called only once and the other calls will wait for the Promise to finish.
94
- The `runningQueries` Map will be cleaned after the method execution is finished.
167
+ The library prevents the cache stampede problem by using a `Map` called `runningQueries` which saves the promise of the function call.
168
+ If multiple calls are made to the same function with the same arguments, the function will be called only once and the other calls will wait for the Promise to finish.
169
+ The `runningQueries` Map will be cleaned after the function execution is finished.
170
+ The `onCacheHit` event will be called also when the running query is returned.
95
171
 
96
172
  ## Considerations on cache operations
97
173
 
98
- The decorator doesn't consider the correct execution of the given cache methods.
99
- It isn't the decorator's responsibility to check if the cache methods are working properly.
100
- The only consideration done is based on the fact that the `set` method will eventually fulfill or reject the Promise as it uses the `.finally` method to delete the key from the `runningQueries` Map and set the timeout to clean the cache record.
174
+ The library doesn't consider the correct execution of the given cache functions.
175
+ It isn't the library's responsibility to check if the cache functions are working properly.
176
+ The only consideration done is based on the fact that the `set` method will eventually fulfill or reject the Promise as it uses the `.finally` Promise method to delete the key from the `runningQueries` Map and set the timeout to clean the cache record.
101
177
 
102
- ## Problems with synchronous methods
178
+ ## Problems with synchronous functions
103
179
 
104
- If the given method is synchronous, the decorator could not work as expected.
105
- The reason is that the decorator internally transforms the method to an asynchronous one, so executing the same method multiple times during the same Event Loop tick will prevent the cache from setting the value in time, thus not working as expected.
180
+ If the given function is synchronous, the library could not work as expected.
181
+ The reason is that the library internally transforms the function to an asynchronous one, so executing the same function multiple times during the same Event Loop tick will prevent the cache from setting the value in time, thus not working as expected.
106
182
  The expected result will still be achieved, but please consider the multiple cache set operations during development.
107
183
 
108
- ## Considerations on identification
109
-
110
- The decorator uses the `instanceIdentifier` and `uniqueIdentifier` to identify the class instance and the method.
111
- The `instanceIdentifier` is generated using the instance properties, so if the instance properties change, the `instanceIdentifier` will change as well, thus changing the data cache key.
112
- The `uniqueIdentifier` is generated using the class name and the method name.
113
-
114
- ## Dictionary
115
-
116
- ### DataCache
184
+ ## Key composition
117
185
 
118
- The effective cache instance.
186
+ The cache key is composed based on the following criteria, allowing multiple files to export the same class name with the same method name / the same function names without conflicts.
119
187
 
120
- ### TimeFrameCache
188
+ ### Higher-order function
121
189
 
122
- The Map containing the method executions in a given timeframe.
190
+ - The arguments passed to the method. (JSON.stringify)
191
+ - `uniqueIdentifier`: A uniqid generated to allow multiple files to contain the same function name.
123
192
 
124
- ### RunningQueryCache
193
+ ### Decorator
125
194
 
126
- The Map containing the running queries (Promises fulfilled or yet to be fulfilled).
195
+ - The method name.
196
+ - `uniqueIdentifier`: A uniqid generated to allow multiple files to contain the same class with the same method.
197
+ - `instanceIdentifier`: A uniqid generated for each instance of the class. It uses the instance properties to generate the id. (JSON.stringify)
198
+ - The arguments passed to the method. (JSON.stringify)
127
199
 
128
- ### Comparation Value
200
+ # Contributing
129
201
 
130
- The value calculated based on candidate conditions which is then compared to the requestThreshold.
202
+ Please, refer to the [CONTRIBUTING.md](https://github.com/JointlyTech/cache-candidate/blob/main/CONTRIBUTING.md) file for more information.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var U=Object.create;var f=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var _=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),q=(e,t)=>{for(var n in t)f(e,n,{get:t[n],enumerable:!0})},x=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of K(t))!j.call(e,a)&&a!==n&&f(e,a,{get:()=>t[a],enumerable:!(r=Q(t,a))||r.enumerable});return e};var H=(e,t,n)=>(n=e!=null?U(G(e)):{},x(t||!e||!e.__esModule?f(n,"default",{value:e,enumerable:!0}):n,e)),J=e=>x(f({},"__esModule",{value:!0}),e);var F=_((ve,v)=>{"use strict";var y=Object.defineProperty,X=Object.getOwnPropertyDescriptor,W=Object.getOwnPropertyNames,$=Object.prototype.hasOwnProperty,B=(e,t)=>{for(var n in t)y(e,n,{get:t[n],enumerable:!0})},Y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of W(t))!$.call(e,a)&&a!==n&&y(e,a,{get:()=>t[a],enumerable:!(r=X(t,a))||r.enumerable});return e},z=e=>Y(y({},"__esModule",{value:!0}),e),N={};B(N,{Hook:()=>ee,hook:()=>te});v.exports=z(N);var V={before:()=>{},after:()=>{}},Z={before:()=>{},after:()=>{}};function ee(e){return function(t,n,r){let a={},i=r.value,{before:o,after:c}={...V,...e};return r.value=function(...u){o({context:a,args:u,target:t,propertyKey:n,descriptor:r});let s=i.apply(this,u);return c({context:a,args:u,target:t,propertyKey:n,descriptor:r,result:s}),s},r}}function te(e){return function(t){let n={},{before:r,after:a}={...Z,...e};return function(...i){r({context:n,args:i});let o=t(...i);return a({context:n,args:i,result:o}),o}}}});var w=_((be,S)=>{"use strict";var D=Object.defineProperty,re=Object.getOwnPropertyDescriptor,ie=Object.getOwnPropertyNames,oe=Object.prototype.hasOwnProperty,ce=(e,t)=>{for(var n in t)D(e,n,{get:t[n],enumerable:!0})},ue=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ie(t))!oe.call(e,a)&&a!==n&&D(e,a,{get:()=>t[a],enumerable:!(r=re(t,a))||r.enumerable});return e},se=e=>ue(D({},"__esModule",{value:!0}),e),b={};ce(b,{Hooks:()=>I});S.exports=se(b);var I=(e=>(e.INIT="INIT",e.EXECUTION_PRE="EXECUTION_PRE",e.EXECUTION_POST="EXECUTION_POST",e.DATACACHE_RECORD_ADD_PRE="DATACACHE_RECORD_ADD_PRE",e.DATACACHE_RECORD_ADD_POST="DATACACHE_RECORD_ADD_POST",e.DATACACHE_RECORD_DELETE_PRE="DATACACHE_RECORD_DELETE_PRE",e.DATACACHE_RECORD_DELETE_POST="DATACACHE_RECORD_DELETE_POST",e.CACHE_HIT="CACHE_HIT",e))(I||{})});var Oe={};q(Oe,{CacheCandidate:()=>Ae,CacheCandidateOptionsDefault:()=>T,DataCacheRecordNotFound:()=>h,Events:()=>p,RunningQueryRecordNotFound:()=>g,cacheCandidate:()=>Re});module.exports=J(Oe);var P=(e=new Map)=>({get:async t=>e.get(JSON.stringify(t)),has:async t=>e.has(JSON.stringify(t)),set:async(t,n)=>e.set(JSON.stringify(t),n),delete:async t=>e.delete(JSON.stringify(t))});var T={ttl:6e5,timeFrame:3e4,requestsThreshold:3,cache:P(),keepAlive:!1,plugins:[],events:{onCacheHit:e=>e,onCacheSet:e=>e,onCacheDelete:e=>e,onBeforeFunctionExecution:e=>e,onAfterFunctionExecution:e=>e,onLog:e=>e}};var p=(a=>(a.RUNNING_QUERY="RUNNING_QUERY",a.CHECKING_CANDIDATE_FUNCTION="CHECKING_CANDIDATE_FUNCTION",a.CHECKING_MILLISECOND_THRESHOLD="CHECKING_MILLISECOND_THRESHOLD",a.CHECKING_REQUESTS_THRESHOLD="CHECKING_REQUESTS_THRESHOLD",a))(p||{}),h=Symbol("DataCacheRecordNotFound"),g=Symbol("RunningQueryRecordNotFound");var M=require("crypto");var ne=H(F());async function C(e,t=[],n){for(let r of t){let a=r.hooks.filter(o=>o.hook===e);if(a.length>1)throw new Error(`Only one hook instance per plugin is allowed. ${r.name} has ${a.length} instances of ${e}}`);if(a.length===0)continue;let i=a[0];if(ae(i))await i.action(n,r.additionalParameters);else throw new Error(`Hook ${e} for plugin ${r.name} is not actionable.`)}}function ae(e){return[e.action!==void 0,typeof e.action=="function"].every(t=>t===!0)}var l=H(w());function de(e,t){return Date.now()<e+t.timeFrame}function Ce({key:e,timeframeCache:t,executionTime:n,executionEnd:r}){t.has(e)?t.get(e).push({executionTime:n,executionEnd:r}):t.set(e,[{executionEnd:r,executionTime:n}])}function le({options:e,key:t,timeframeCache:n}){if(n.has(t)){let r=n.get(t),a=r.filter(({executionEnd:i})=>de(i,e));r.length!==a.length&&n.set(t,a)}}function me({options:e,key:t,runningQueryCache:n}){return n.has(t)?(e.events.onLog({key:t,event:"RUNNING_QUERY"}),n.get(t)):g}function A(...e){return(0,M.createHash)("sha256").update(e.join("|")).digest("hex")}function he({birthTime:e,options:t}){return Date.now()-e>=t.timeFrame}async function fe({options:e,key:t,HookPayload:n}){if(await e.cache.has(t)){let{result:r,birthTime:a}=await e.cache.get(t);return he({birthTime:a,options:e})?(await k({options:e,key:t,HookPayload:n}),h):r}return h}async function ge({options:e,key:t,result:n}){return e.cache.set(t,{result:n,birthTime:Date.now()},e.ttl)}async function k({options:e,key:t,HookPayload:n}){await C(l.Hooks.DATACACHE_RECORD_DELETE_PRE,e.plugins,n),await e.cache.delete(t),await C(l.Hooks.DATACACHE_RECORD_DELETE_POST,e.plugins,n),e.events.onCacheDelete({key:t})}async function L({result:e,runningQueryCache:t,key:n,executionStart:r,options:a,timeframeCache:i,timeFrameTimeoutCache:o,args:c,HookPayload:u}){let s=Date.now(),m=s-r;a.events.onAfterFunctionExecution({key:n,executionTime:m}),Ce({key:n,timeframeCache:i,executionTime:m,executionEnd:s}),Ee({options:a,key:n,timeframeCache:i,executionTime:m,args:c})>=a.requestsThreshold&&(await C(l.Hooks.DATACACHE_RECORD_ADD_PRE,a.plugins,u),ge({options:a,key:n,result:e}).then(async()=>{await C(l.Hooks.DATACACHE_RECORD_ADD_POST,a.plugins,{...u,result:e}),a.events.onCacheSet({key:n})}).finally(()=>{t.delete(n),o.set(n,setTimeout(()=>{k({options:a,key:n,HookPayload:u})},a.ttl).unref())}))}function Ee({options:e,key:t,timeframeCache:n,executionTime:r,args:a}){let i=0,o=n.get(t);return e.candidateFunction?(e.events.onLog({key:t,event:"CHECKING_CANDIDATE_FUNCTION"}),i=pe(e,r,a,o)):e.millisecondThreshold?(e.events.onLog({key:t,event:"CHECKING_MILLISECOND_THRESHOLD"}),i=Te(r,e,o)):(e.events.onLog({key:t,event:"CHECKING_REQUESTS_THRESHOLD"}),i=o.length),i}function Te(e,t,n){let r=0;return e>t.millisecondThreshold&&(r=n.filter(a=>a.executionTime>t.millisecondThreshold).length),r}function pe(e,t,n,r){let a=0;return e.candidateFunction({timeFrameCacheRecords:r,options:e,args:n})&&(a=e.requestsThreshold),a}function ye({timeFrameTimeoutCache:e,key:t,options:n}){clearTimeout(e.get(t)),e.set(t,setTimeout(()=>{n.cache.delete(t)},n.ttl).unref())}function De(e=10){return Math.random().toString(36).substring(2,e+2)}async function R({options:e,key:t,timeFrameTimeoutCache:n,runningQueryCache:r,timeframeCache:a,args:i,originalMethod:o}){let c={options:{...e,plugins:void 0},key:t,timeFrameTimeoutCache:n,runningQueryCache:r,timeframeCache:a,fnArgs:i};await C(l.Hooks.INIT,e.plugins,c);let u=await fe({options:e,key:t,HookPayload:c});if(u!==h)return e.keepAlive&&ye({timeFrameTimeoutCache:n,key:t,options:e}),await C(l.Hooks.CACHE_HIT,e.plugins,{...c,result:u}),e.events.onCacheHit({key:t}),Promise.resolve(u);let s=me({options:e,key:t,runningQueryCache:r});if(s!==g)return await C(l.Hooks.CACHE_HIT,e.plugins,{...c,result:s}),e.events.onCacheHit({key:t}),s;le({options:e,key:t,timeframeCache:a}),await C(l.Hooks.EXECUTION_PRE,e.plugins,c),e.events.onBeforeFunctionExecution({key:t});let m=Date.now(),d=o(...i);return await C(l.Hooks.EXECUTION_POST,e.plugins,{...c,result:d}),d instanceof Promise?(r.set(t,d),d.then(E=>L({result:E,runningQueryCache:r,key:t,executionStart:m,options:e,timeframeCache:a,timeFrameTimeoutCache:n,args:i,HookPayload:c})),d):(L({result:d,runningQueryCache:r,key:t,executionStart:m,options:e,timeframeCache:a,timeFrameTimeoutCache:n,args:i,HookPayload:c}),d)}function O(e){let t=new Map,n=new Map,r=De(),a=new Map,i={...T,...e};return{timeframeCache:t,runningQueryCache:n,uniqueIdentifier:r,timeFrameTimeoutCache:a,options:i}}function Ae(e={}){let{timeframeCache:t,runningQueryCache:n,uniqueIdentifier:r,timeFrameTimeoutCache:a,options:i}=O(e);return function(o,c,u){let s=u.value;u.value=async function(...m){let d=o.constructor.name+JSON.stringify(this),E=A([o.constructor.name,c,r,d,JSON.stringify(m)]);return R({options:i,key:E,timeFrameTimeoutCache:a,runningQueryCache:n,timeframeCache:t,args:m,originalMethod:s})}}}function Re(e,t={}){let{timeframeCache:n,runningQueryCache:r,uniqueIdentifier:a,timeFrameTimeoutCache:i,options:o}=O(t);return async(...c)=>R({options:o,key:A([a,JSON.stringify(c)]),timeFrameTimeoutCache:i,runningQueryCache:r,timeframeCache:n,args:c,originalMethod:e})}0&&(module.exports={CacheCandidate,CacheCandidateOptionsDefault,DataCacheRecordNotFound,Events,RunningQueryRecordNotFound,cacheCandidate});
1
+ "use strict";var $=Object.create;var f=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var _=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),K=(e,t)=>{for(var n in t)f(e,n,{get:t[n],enumerable:!0})},b=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of U(t))!X.call(e,a)&&a!==n&&f(e,a,{get:()=>t[a],enumerable:!(r=L(t,a))||r.enumerable});return e};var y=(e,t,n)=>(n=e!=null?$(W(e)):{},b(t||!e||!e.__esModule?f(n,"default",{value:e,enumerable:!0}):n,e)),B=e=>b(f({},"__esModule",{value:!0}),e);var A=_((we,w)=>{"use strict";var E=Object.defineProperty,z=Object.getOwnPropertyDescriptor,G=Object.getOwnPropertyNames,V=Object.prototype.hasOwnProperty,Y=(e,t)=>{for(var n in t)E(e,n,{get:t[n],enumerable:!0})},Z=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of G(t))!V.call(e,a)&&a!==n&&E(e,a,{get:()=>t[a],enumerable:!(r=z(t,a))||r.enumerable});return e},ee=e=>Z(E({},"__esModule",{value:!0}),e),F={};Y(F,{Hooks:()=>H});w.exports=ee(F);var H=(e=>(e.INIT="INIT",e.EXECUTION_PRE="EXECUTION_PRE",e.EXECUTION_POST="EXECUTION_POST",e.DATACACHE_RECORD_ADD_PRE="DATACACHE_RECORD_ADD_PRE",e.DATACACHE_RECORD_ADD_POST="DATACACHE_RECORD_ADD_POST",e.DATACACHE_RECORD_DELETE_PRE="DATACACHE_RECORD_DELETE_PRE",e.DATACACHE_RECORD_DELETE_POST="DATACACHE_RECORD_DELETE_POST",e.CACHE_HIT="CACHE_HIT",e))(H||{})});var N=_((ke,S)=>{"use strict";var D=Object.defineProperty,te=Object.getOwnPropertyDescriptor,ne=Object.getOwnPropertyNames,ae=Object.prototype.hasOwnProperty,re=(e,t)=>{for(var n in t)D(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ne(t))!ae.call(e,a)&&a!==n&&D(e,a,{get:()=>t[a],enumerable:!(r=te(t,a))||r.enumerable});return e},ie=e=>oe(D({},"__esModule",{value:!0}),e),k={};re(k,{Hook:()=>se,hook:()=>de});S.exports=ie(k);var ce={before:()=>{},after:()=>{}},ue={before:()=>{},after:()=>{}};function se(e){return function(t,n,r){let a={},o=r.value,{before:i,after:c}={...ce,...e};return r.value=function(...u){i({context:a,args:u,target:t,propertyKey:n,descriptor:r});let s=o.apply(this,u);return c({context:a,args:u,target:t,propertyKey:n,descriptor:r,result:s}),s},r}}function de(e){return function(t){let n={},{before:r,after:a}={...ue,...e};return function(...o){r({context:n,args:o});let i=t(...o);return a({context:n,args:o,result:i}),i}}}});var Pe={};K(Pe,{CacheCandidate:()=>Re,CacheCandidateOptionsDefault:()=>T,DataCacheRecordNotFound:()=>m,RunningQueryRecordNotFound:()=>g,cacheCandidate:()=>xe});module.exports=B(Pe);var v=(e=new Map)=>({get:async t=>e.get(JSON.stringify(t)),has:async t=>e.has(JSON.stringify(t)),set:async(t,n)=>e.set(JSON.stringify(t),n),delete:async t=>e.delete(JSON.stringify(t))});var T={ttl:6e5,timeFrame:3e4,requestsThreshold:3,cache:v(),keepAlive:!1,plugins:[],events:{onCacheHit:e=>e,onCacheSet:e=>e,onCacheDelete:e=>e,onBeforeFunctionExecution:e=>e,onAfterFunctionExecution:e=>e}};var m=Symbol("DataCacheRecordNotFound"),g=Symbol("RunningQueryRecordNotFound");var q=require("crypto");var I=y(A()),M=y(N());async function l(e,t=[],n){for(let r of t){let a=r.hooks.filter(i=>i.hook===e);if(a.length>1)throw new Error(`Only one hook instance per plugin is allowed. ${r.name} has ${a.length} instances of ${e}}`);if(a.length===0)continue;let o=a[0];if(le(o))await o.action(n,r.additionalParameters);else throw new Error(`Hook ${e} for plugin ${r.name} is not actionable.`)}}function le(e){return[e.action!==void 0,typeof e.action=="function"].every(t=>t===!0)}function j(e,t,n){return(0,M.hook)({before:({args:r})=>{l(e,r[0].plugins,n)},after:({args:r,result:a})=>{l(t,r[0].plugins,{...n,result:a})}})}function O({options:e}){e.plugins&&e.plugins.forEach(t=>{if(!t.hooks||!Array.isArray(t.hooks)||t.hooks.length===0)throw new Error(`Plugin ${t.name} has no hooks.`);t.hooks.forEach(n=>{if(!I.Hooks[n.hook])throw new Error(`Invalid hook ${n}`)})})}var h=y(A());function he(e,t){return Date.now()<e+t.timeFrame}function Ce({key:e,timeframeCache:t,executionTime:n,executionEnd:r}){t.has(e)?t.get(e).push({executionTime:n,executionEnd:r}):t.set(e,[{executionEnd:r,executionTime:n}])}function me({options:e,key:t,timeframeCache:n}){if(n.has(t)){let r=n.get(t),a=r.filter(({executionEnd:o})=>he(o,e));r.length!==a.length&&n.set(t,a)}}function fe({key:e,runningQueryCache:t}){return t.has(e)?t.get(e):g}function R(...e){return(0,q.createHash)("sha256").update(e.join("|")).digest("hex")}function ge({birthTime:e,options:t}){return Date.now()-e>=t.ttl}async function pe({options:e,key:t,HookPayload:n}){if(await e.cache.has(t)){let{result:r,birthTime:a}=await e.cache.get(t);return ge({birthTime:a,options:e})?(await J({options:e,key:t,HookPayload:n}),m):r}return m}async function ye({options:e,key:t,result:n}){return e.cache.set(t,{result:n,birthTime:Date.now()},e.ttl)}async function J({options:e,key:t,HookPayload:n}){await j(h.Hooks.DATACACHE_RECORD_DELETE_PRE,h.Hooks.DATACACHE_RECORD_DELETE_POST,n)(e.cache.delete)(t),e.events.onCacheDelete({key:t})}async function Q({result:e,runningQueryCache:t,key:n,executionStart:r,options:a,timeframeCache:o,timeFrameTimeoutCache:i,args:c,HookPayload:u}){let s=Date.now(),C=s-r;a.events.onAfterFunctionExecution({key:n,executionTime:C}),Ce({key:n,timeframeCache:o,executionTime:C,executionEnd:s}),Te({options:a,key:n,timeframeCache:o,executionTime:C,args:c})>=a.requestsThreshold?(await l(h.Hooks.DATACACHE_RECORD_ADD_PRE,a.plugins,u),ye({options:a,key:n,result:e}).then(async()=>{await l(h.Hooks.DATACACHE_RECORD_ADD_POST,a.plugins,{...u,result:e}),a.events.onCacheSet({key:n})}).finally(()=>{t.delete(n),i.set(n,setTimeout(()=>{J({options:a,key:n,HookPayload:u})},a.ttl).unref())})):t.delete(n)}function Te({options:e,key:t,timeframeCache:n,executionTime:r,args:a}){let o=0,i=n.get(t);return e.candidateFunction?o=Ae(e,r,a,i):e.millisecondThreshold?o=Ee(r,e,i):o=i.length,o}function Ee(e,t,n){let r=0;return e>t.millisecondThreshold&&(r=n.filter(a=>a.executionTime>t.millisecondThreshold).length),r}function Ae(e,t,n,r){let a=0;return e.candidateFunction({timeFrameCacheRecords:r,options:e,args:n})&&(a=e.requestsThreshold),a}function De({timeFrameTimeoutCache:e,key:t,options:n}){clearTimeout(e.get(t)),e.set(t,setTimeout(()=>{n.cache.delete(t)},n.ttl).unref())}function Oe(e=10){return Math.random().toString(36).substring(2,e+2)}async function x({options:e,key:t,timeFrameTimeoutCache:n,runningQueryCache:r,timeframeCache:a,args:o,originalMethod:i}){let c={options:{...e,plugins:void 0},key:t,timeFrameTimeoutCache:n,runningQueryCache:r,timeframeCache:a,fnArgs:o};await l(h.Hooks.INIT,e.plugins,c);let u=await pe({options:e,key:t,HookPayload:c});if(u!==m)return e.keepAlive&&De({timeFrameTimeoutCache:n,key:t,options:e}),await l(h.Hooks.CACHE_HIT,e.plugins,{...c,result:u}),e.events.onCacheHit({key:t}),Promise.resolve(u);let s=fe({key:t,runningQueryCache:r});if(s!==g)return await l(h.Hooks.CACHE_HIT,e.plugins,{...c,result:s}),e.events.onCacheHit({key:t}),s;me({options:e,key:t,timeframeCache:a}),await l(h.Hooks.EXECUTION_PRE,e.plugins,c),e.events.onBeforeFunctionExecution({key:t});let C=Date.now(),d=i(...o);return await l(h.Hooks.EXECUTION_POST,e.plugins,{...c,result:d}),d instanceof Promise?(r.set(t,d),d.then(p=>Q({result:p,runningQueryCache:r,key:t,executionStart:C,options:e,timeframeCache:a,timeFrameTimeoutCache:n,args:o,HookPayload:c})),d):(Q({result:d,runningQueryCache:r,key:t,executionStart:C,options:e,timeframeCache:a,timeFrameTimeoutCache:n,args:o,HookPayload:c}),d)}function P(e){let t=new Map,n=new Map,r=Oe(),a=new Map,o={...T,...e};return{timeframeCache:t,runningQueryCache:n,uniqueIdentifier:r,timeFrameTimeoutCache:a,options:o}}function Re(e={}){let{timeframeCache:t,runningQueryCache:n,uniqueIdentifier:r,timeFrameTimeoutCache:a,options:o}=P(e);return O({options:o}),function(i,c,u){let s=u.value;u.value=async function(...C){let d=i.constructor.name+JSON.stringify(this),p=R([c,r,d,JSON.stringify(C)]);return x({options:o,key:p,timeFrameTimeoutCache:a,runningQueryCache:n,timeframeCache:t,args:C,originalMethod:s})}}}function xe(e,t={}){let{timeframeCache:n,runningQueryCache:r,uniqueIdentifier:a,timeFrameTimeoutCache:o,options:i}=P(t);return O({options:i}),async(...c)=>x({options:i,key:R([a,JSON.stringify(c)]),timeFrameTimeoutCache:o,runningQueryCache:r,timeframeCache:n,args:c,originalMethod:e})}0&&(module.exports={CacheCandidate,CacheCandidateOptionsDefault,DataCacheRecordNotFound,RunningQueryRecordNotFound,cacheCandidate});
package/dist/models.d.ts CHANGED
@@ -36,10 +36,6 @@ export interface CacheCandidateOptions {
36
36
  key: string;
37
37
  executionTime: number;
38
38
  }) => void;
39
- onLog: ({ key, event }: {
40
- key: string;
41
- event: Events;
42
- }) => void;
43
39
  };
44
40
  plugins?: Array<CacheCandidatePluginWithAdditionalParameters>;
45
41
  }
@@ -54,11 +50,5 @@ export interface TimeFrameCacheRecord {
54
50
  export type TimeFrameCache = Map<string, Array<TimeFrameCacheRecord>>;
55
51
  export type RunningQueryCache = Map<string, Promise<any>>;
56
52
  export type TimeFrameTimeoutCache = Map<string, NodeJS.Timeout>;
57
- export declare enum Events {
58
- RUNNING_QUERY = "RUNNING_QUERY",
59
- CHECKING_CANDIDATE_FUNCTION = "CHECKING_CANDIDATE_FUNCTION",
60
- CHECKING_MILLISECOND_THRESHOLD = "CHECKING_MILLISECOND_THRESHOLD",
61
- CHECKING_REQUESTS_THRESHOLD = "CHECKING_REQUESTS_THRESHOLD"
62
- }
63
53
  export declare const DataCacheRecordNotFound: unique symbol;
64
54
  export declare const RunningQueryRecordNotFound: unique symbol;
@@ -1,3 +1,7 @@
1
1
  import { CacheCandidatePluginWithAdditionalParameters, Hooks, PluginPayload } from '@jointly/cache-candidate-plugin-base';
2
+ import { CacheCandidateOptions } from '../models';
2
3
  export declare function ExecuteHook(hook: Hooks, plugins: CacheCandidatePluginWithAdditionalParameters[] | undefined, payload: PluginPayload): Promise<void>;
3
4
  export declare function pluginHookWrap(HookBefore: Hooks, HookAfter: Hooks, payload: PluginPayload): (fn: (...args: any[]) => unknown) => (...args: any[]) => unknown;
5
+ export declare function checkHooks({ options }: {
6
+ options: CacheCandidateOptions;
7
+ }): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jointly/cache-candidate",
3
3
  "private": false,
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -34,7 +34,8 @@
34
34
  "devDependencies": {
35
35
  "@commitlint/cli": "^17.3.0",
36
36
  "@commitlint/config-conventional": "^17.3.0",
37
- "@types/jest": "^29.2.4",
37
+ "@jointly/cache-candidate-plugin-base": "^1.0.0",
38
+ "@types/jest": "^29.2.5",
38
39
  "@types/node": "^18.11.12",
39
40
  "@types/redis": "^4.0.11",
40
41
  "@typescript-eslint/eslint-plugin": "^5.46.0",
@@ -48,8 +49,7 @@
48
49
  "prettier": "^2.8.1",
49
50
  "rimraf": "^3.0.2",
50
51
  "ts-jest": "^29.0.3",
51
- "typescript": "^4.9.4",
52
- "@jointly/cache-candidate-plugin-base": "^1.0.0"
52
+ "typescript": "^4.9.4"
53
53
  },
54
54
  "lint-staged": {
55
55
  "src/**/*.{js,jsx,ts,tsx}": [