@twin.org/core 0.0.1-next.52 → 0.0.1-next.54

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.
@@ -4006,20 +4006,88 @@ class AsyncCache {
4006
4006
  * @param key The key for the entry in the cache.
4007
4007
  * @param ttlMs The TTL of the entry in the cache.
4008
4008
  * @param requestMethod The method to call if not cached.
4009
+ * @param cacheFailures Cache failure results, defaults to false.
4009
4010
  * @returns The response.
4010
4011
  */
4011
- static exec(key, ttlMs, requestMethod) {
4012
+ static exec(key, ttlMs, requestMethod, cacheFailures) {
4012
4013
  const cacheEnabled = Is.integer(ttlMs) && ttlMs >= 0;
4013
4014
  if (cacheEnabled) {
4014
4015
  AsyncCache.cleanupExpired();
4015
4016
  const cache = AsyncCache.getSharedCache();
4016
- if (!cache[key]) {
4017
- cache[key] = {
4018
- response: requestMethod(),
4019
- expires: ttlMs === 0 ? 0 : Date.now() + ttlMs
4020
- };
4017
+ // Do we have a cache entry for the key
4018
+ if (cache[key]) {
4019
+ if (!Is.empty(cache[key].result)) {
4020
+ // If the cache has already resulted in a value, resolve it
4021
+ return Promise.resolve(cache[key].result);
4022
+ }
4023
+ else if (!Is.empty(cache[key].error)) {
4024
+ // If the cache has already resulted in an error, reject it
4025
+ return Promise.reject(cache[key].error);
4026
+ }
4027
+ // Otherwise create a promise to return and store the resolver
4028
+ // and rejector in the cache entry, so that we can call then
4029
+ // when the request is done
4030
+ let storedResolve;
4031
+ let storedReject;
4032
+ const wait = new Promise((resolve, reject) => {
4033
+ storedResolve = resolve;
4034
+ storedReject = reject;
4035
+ });
4036
+ if (!Is.empty(storedResolve) && !Is.empty(storedReject)) {
4037
+ cache[key].promiseQueue.push({
4038
+ requestMethod,
4039
+ resolve: storedResolve,
4040
+ reject: storedReject
4041
+ });
4042
+ }
4043
+ return wait;
4021
4044
  }
4022
- return cache[key].response;
4045
+ // If we don't have a cache entry, create a new one
4046
+ cache[key] = {
4047
+ promiseQueue: [],
4048
+ expires: ttlMs === 0 ? 0 : Date.now() + ttlMs
4049
+ };
4050
+ // Return a promise that wraps the original request method
4051
+ // so that we can store any results or errors in the cache
4052
+ return new Promise((resolve, reject) => {
4053
+ // Call the request method and store the result
4054
+ requestMethod()
4055
+ // eslint-disable-next-line promise/prefer-await-to-then
4056
+ .then(res => {
4057
+ // If the request was successful, store the result
4058
+ cache[key].result = res;
4059
+ // and resolve both this promise and all the waiters
4060
+ resolve(res);
4061
+ for (const wait of cache[key].promiseQueue) {
4062
+ wait.resolve(res);
4063
+ }
4064
+ return res;
4065
+ })
4066
+ // eslint-disable-next-line promise/prefer-await-to-then
4067
+ .catch((err) => {
4068
+ // Reject the promise
4069
+ reject(err);
4070
+ // Handle the waiters based on the cacheFailures flag
4071
+ if (cacheFailures ?? false) {
4072
+ // If we are caching failures, store the error and reject the waiters
4073
+ cache[key].error = err;
4074
+ for (const wait of cache[key].promiseQueue) {
4075
+ wait.reject(err);
4076
+ }
4077
+ // Clear the waiters so we don't call them again
4078
+ cache[key].promiseQueue = [];
4079
+ }
4080
+ else {
4081
+ // If not caching failures for any queued requests we
4082
+ // have no value to either resolve or reject, so we
4083
+ // just resolve with the original request method
4084
+ for (const wait of cache[key].promiseQueue) {
4085
+ wait.resolve(wait.requestMethod());
4086
+ }
4087
+ delete cache[key];
4088
+ }
4089
+ });
4090
+ });
4023
4091
  }
4024
4092
  }
4025
4093
  /**
@@ -4029,7 +4097,14 @@ class AsyncCache {
4029
4097
  */
4030
4098
  static async get(key) {
4031
4099
  const cache = AsyncCache.getSharedCache();
4032
- return cache[key]?.response;
4100
+ if (!Is.empty(cache[key].result)) {
4101
+ // If the cache has already resulted in a value, resolve it
4102
+ return cache[key].result;
4103
+ }
4104
+ else if (!Is.empty(cache[key].error)) {
4105
+ // If the cache has already resulted in an error, reject it
4106
+ throw cache[key].error;
4107
+ }
4033
4108
  }
4034
4109
  /**
4035
4110
  * Set an entry into the cache.
@@ -4041,7 +4116,8 @@ class AsyncCache {
4041
4116
  static async set(key, value, ttlMs) {
4042
4117
  const cache = AsyncCache.getSharedCache();
4043
4118
  cache[key] = {
4044
- response: Promise.resolve(value),
4119
+ result: value,
4120
+ promiseQueue: [],
4045
4121
  expires: Date.now() + (ttlMs ?? 1000)
4046
4122
  };
4047
4123
  }
@@ -4004,20 +4004,88 @@ class AsyncCache {
4004
4004
  * @param key The key for the entry in the cache.
4005
4005
  * @param ttlMs The TTL of the entry in the cache.
4006
4006
  * @param requestMethod The method to call if not cached.
4007
+ * @param cacheFailures Cache failure results, defaults to false.
4007
4008
  * @returns The response.
4008
4009
  */
4009
- static exec(key, ttlMs, requestMethod) {
4010
+ static exec(key, ttlMs, requestMethod, cacheFailures) {
4010
4011
  const cacheEnabled = Is.integer(ttlMs) && ttlMs >= 0;
4011
4012
  if (cacheEnabled) {
4012
4013
  AsyncCache.cleanupExpired();
4013
4014
  const cache = AsyncCache.getSharedCache();
4014
- if (!cache[key]) {
4015
- cache[key] = {
4016
- response: requestMethod(),
4017
- expires: ttlMs === 0 ? 0 : Date.now() + ttlMs
4018
- };
4015
+ // Do we have a cache entry for the key
4016
+ if (cache[key]) {
4017
+ if (!Is.empty(cache[key].result)) {
4018
+ // If the cache has already resulted in a value, resolve it
4019
+ return Promise.resolve(cache[key].result);
4020
+ }
4021
+ else if (!Is.empty(cache[key].error)) {
4022
+ // If the cache has already resulted in an error, reject it
4023
+ return Promise.reject(cache[key].error);
4024
+ }
4025
+ // Otherwise create a promise to return and store the resolver
4026
+ // and rejector in the cache entry, so that we can call then
4027
+ // when the request is done
4028
+ let storedResolve;
4029
+ let storedReject;
4030
+ const wait = new Promise((resolve, reject) => {
4031
+ storedResolve = resolve;
4032
+ storedReject = reject;
4033
+ });
4034
+ if (!Is.empty(storedResolve) && !Is.empty(storedReject)) {
4035
+ cache[key].promiseQueue.push({
4036
+ requestMethod,
4037
+ resolve: storedResolve,
4038
+ reject: storedReject
4039
+ });
4040
+ }
4041
+ return wait;
4019
4042
  }
4020
- return cache[key].response;
4043
+ // If we don't have a cache entry, create a new one
4044
+ cache[key] = {
4045
+ promiseQueue: [],
4046
+ expires: ttlMs === 0 ? 0 : Date.now() + ttlMs
4047
+ };
4048
+ // Return a promise that wraps the original request method
4049
+ // so that we can store any results or errors in the cache
4050
+ return new Promise((resolve, reject) => {
4051
+ // Call the request method and store the result
4052
+ requestMethod()
4053
+ // eslint-disable-next-line promise/prefer-await-to-then
4054
+ .then(res => {
4055
+ // If the request was successful, store the result
4056
+ cache[key].result = res;
4057
+ // and resolve both this promise and all the waiters
4058
+ resolve(res);
4059
+ for (const wait of cache[key].promiseQueue) {
4060
+ wait.resolve(res);
4061
+ }
4062
+ return res;
4063
+ })
4064
+ // eslint-disable-next-line promise/prefer-await-to-then
4065
+ .catch((err) => {
4066
+ // Reject the promise
4067
+ reject(err);
4068
+ // Handle the waiters based on the cacheFailures flag
4069
+ if (cacheFailures ?? false) {
4070
+ // If we are caching failures, store the error and reject the waiters
4071
+ cache[key].error = err;
4072
+ for (const wait of cache[key].promiseQueue) {
4073
+ wait.reject(err);
4074
+ }
4075
+ // Clear the waiters so we don't call them again
4076
+ cache[key].promiseQueue = [];
4077
+ }
4078
+ else {
4079
+ // If not caching failures for any queued requests we
4080
+ // have no value to either resolve or reject, so we
4081
+ // just resolve with the original request method
4082
+ for (const wait of cache[key].promiseQueue) {
4083
+ wait.resolve(wait.requestMethod());
4084
+ }
4085
+ delete cache[key];
4086
+ }
4087
+ });
4088
+ });
4021
4089
  }
4022
4090
  }
4023
4091
  /**
@@ -4027,7 +4095,14 @@ class AsyncCache {
4027
4095
  */
4028
4096
  static async get(key) {
4029
4097
  const cache = AsyncCache.getSharedCache();
4030
- return cache[key]?.response;
4098
+ if (!Is.empty(cache[key].result)) {
4099
+ // If the cache has already resulted in a value, resolve it
4100
+ return cache[key].result;
4101
+ }
4102
+ else if (!Is.empty(cache[key].error)) {
4103
+ // If the cache has already resulted in an error, reject it
4104
+ throw cache[key].error;
4105
+ }
4031
4106
  }
4032
4107
  /**
4033
4108
  * Set an entry into the cache.
@@ -4039,7 +4114,8 @@ class AsyncCache {
4039
4114
  static async set(key, value, ttlMs) {
4040
4115
  const cache = AsyncCache.getSharedCache();
4041
4116
  cache[key] = {
4042
- response: Promise.resolve(value),
4117
+ result: value,
4118
+ promiseQueue: [],
4043
4119
  expires: Date.now() + (ttlMs ?? 1000)
4044
4120
  };
4045
4121
  }
@@ -7,9 +7,10 @@ export declare class AsyncCache {
7
7
  * @param key The key for the entry in the cache.
8
8
  * @param ttlMs The TTL of the entry in the cache.
9
9
  * @param requestMethod The method to call if not cached.
10
+ * @param cacheFailures Cache failure results, defaults to false.
10
11
  * @returns The response.
11
12
  */
12
- static exec<T = unknown>(key: string, ttlMs: number | undefined, requestMethod: () => Promise<T>): Promise<T> | undefined;
13
+ static exec<T = unknown>(key: string, ttlMs: number | undefined, requestMethod: () => Promise<T>, cacheFailures?: boolean): Promise<T> | undefined;
13
14
  /**
14
15
  * Get an entry from the cache.
15
16
  * @param key The key to get from the cache.
package/docs/changelog.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @twin.org/core - Changelog
2
2
 
3
+ ## [0.0.1-next.54](https://github.com/twinfoundation/framework/compare/core-v0.0.1-next.53...core-v0.0.1-next.54) (2025-05-06)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **core:** Synchronize repo versions
9
+
10
+ ## [0.0.1-next.53](https://github.com/twinfoundation/framework/compare/core-v0.0.1-next.52...core-v0.0.1-next.53) (2025-05-01)
11
+
12
+
13
+ ### Features
14
+
15
+ * async cache don't cache failures unless requested ([658ec4b](https://github.com/twinfoundation/framework/commit/658ec4b67a58a075de4702a3886d151e25ad3ddc))
16
+
3
17
  ## [0.0.1-next.52](https://github.com/twinfoundation/framework/compare/core-v0.0.1-next.51...core-v0.0.1-next.52) (2025-04-17)
4
18
 
5
19
 
@@ -16,7 +16,7 @@ Cache the results from asynchronous requests.
16
16
 
17
17
  ### exec()
18
18
 
19
- > `static` **exec**\<`T`\>(`key`, `ttlMs`, `requestMethod`): `undefined` \| `Promise`\<`T`\>
19
+ > `static` **exec**\<`T`\>(`key`, `ttlMs`, `requestMethod`, `cacheFailures?`): `undefined` \| `Promise`\<`T`\>
20
20
 
21
21
  Execute an async request and cache the result.
22
22
 
@@ -46,6 +46,12 @@ The TTL of the entry in the cache.
46
46
 
47
47
  The method to call if not cached.
48
48
 
49
+ ##### cacheFailures?
50
+
51
+ `boolean`
52
+
53
+ Cache failure results, defaults to false.
54
+
49
55
  #### Returns
50
56
 
51
57
  `undefined` \| `Promise`\<`T`\>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/core",
3
- "version": "0.0.1-next.52",
3
+ "version": "0.0.1-next.54",
4
4
  "description": "Helper methods/classes for data type checking/validation/guarding/error handling",
5
5
  "repository": {
6
6
  "type": "git",