@raketa-cloud/next 1.0.9 → 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 ADDED
@@ -0,0 +1,144 @@
1
+ # @raketa-cloud/next
2
+
3
+ Next.js integration library for **Raketa Cloud DXP**. Provides GraphQL API clients, in-memory LRU caching, and dynamic URL redirect helpers for Next.js App Router applications.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @raketa-cloud/next
11
+ ```
12
+
13
+ Peer & runtime dependencies:
14
+
15
+ ```json
16
+ {
17
+ "dependencies": {
18
+ "graphql-request": "^7.1.2",
19
+ "lru-cache": "^11.0.2",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Exports
29
+
30
+ ```javascript
31
+ import {
32
+ Cache,
33
+ DxpPublic,
34
+ DxpPrivate,
35
+ generateCloudPublicHelpers,
36
+ } from "@raketa-cloud/next";
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Quick Reference
42
+
43
+ ### 1. In-Memory Cache (`Cache`)
44
+
45
+ ```javascript
46
+ import { Cache } from "@raketa-cloud/next";
47
+
48
+ const cache = new Cache({
49
+ max: 2500, // Max entries
50
+ maxSize: 300 * 1024 * 1024, // 300 MB budget
51
+ ttl: 1000 * 60 * 5, // 5 minutes
52
+ debugMode: false,
53
+ });
54
+
55
+ // Fetch with cache & stale-while-revalidate fallback:
56
+ const data = await cache.fetch("my-cache-key", async () => {
57
+ return await fetchFromUpstream();
58
+ });
59
+ ```
60
+
61
+ ### 2. Public DXP Client (`DxpPublic`)
62
+
63
+ ```javascript
64
+ import { DxpPublic } from "@raketa-cloud/next";
65
+ import getConfig from "lib/getConfig";
66
+ import localCache from "./localCache";
67
+
68
+ const dxpPublic = new DxpPublic(getConfig, localCache);
69
+
70
+ // Find a single resource (e.g. page, article, customer)
71
+ const page = await dxpPublic.resources.find("en", "about-us", "page", {
72
+ withCache: true,
73
+ });
74
+
75
+ // List resources with filters & pagination
76
+ const articles = await dxpPublic.resources.list("en", {
77
+ pagination: { page: 1, pageSize: 10 },
78
+ filters: { resourceType: "article", tagSlugs: ["news"] },
79
+ });
80
+
81
+ // Fetch website settings
82
+ const settings = await dxpPublic.settings.find("en", { withCache: true });
83
+
84
+ // Fetch a tag
85
+ const tag = await dxpPublic.tags.find("en", "technology", { withCache: true });
86
+
87
+ // Custom GraphQL query
88
+ const queryDoc = dxpPublic.client.gql`
89
+ query MyQuery($input: ResourcesInput!) {
90
+ resources(input: $input) {
91
+ list { id title slug }
92
+ }
93
+ }
94
+ `;
95
+ const result = await dxpPublic.client.query(queryDoc, {
96
+ input: { locale: "en" },
97
+ });
98
+ ```
99
+
100
+ ### 3. Dynamic Redirects Helper (`generateCloudPublicHelpers`)
101
+
102
+ ```javascript
103
+ import { DxpPublic, generateCloudPublicHelpers } from "@raketa-cloud/next";
104
+ import getConfig from "lib/getConfig";
105
+ import redirectsCache from "./redirectsCache";
106
+
107
+ const dxpRedirects = new DxpPublic(getConfig, redirectsCache);
108
+ const { findRedirect } = generateCloudPublicHelpers(dxpRedirects);
109
+
110
+ // In Next.js middleware:
111
+ const redirect = await findRedirect("/old-path");
112
+ // returns: { id, url, destination, httpResponseCode } | null
113
+ ```
114
+
115
+ ### 4. Private DXP Client (`DxpPrivate`)
116
+
117
+ ```javascript
118
+ import { DxpPrivate } from "@raketa-cloud/next";
119
+ import getConfig from "lib/getConfig";
120
+
121
+ // Server-side (with token resolver function)
122
+ const dxpPrivate = new DxpPrivate(getConfig, async () => {
123
+ return getAdminTokenFromCookies();
124
+ });
125
+
126
+ // Change preview for visual builder
127
+ const preview = await dxpPrivate.resources.changePreview(
128
+ "en",
129
+ resourceId,
130
+ versionNumber,
131
+ "page",
132
+ );
133
+
134
+ // Mini-resources list for admin pickers
135
+ const miniList = await dxpPrivate.resources.miniResources("en", {
136
+ filters: { resourceType: "page" },
137
+ });
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Documentation
143
+
144
+ For a comprehensive guide with complete project structure and Next.js App Router patterns, see the [Monorepo README](../../README.md).
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@raketa-cloud/next",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
+ "type": "module",
4
5
  "main": "src/index.js",
5
6
  "repository": {
6
7
  "type": "git",
@@ -8,15 +9,16 @@
8
9
  "directory": "packages/raketa-cloud-next"
9
10
  },
10
11
  "scripts": {
12
+ "test": "node --test test/**/*.test.js",
11
13
  "check-types": "tsc --noEmit",
12
14
  "format": "prettier --write src/**/*.js"
13
15
  },
14
16
  "devDependencies": {
15
17
  "@repo/typescript-config": "*",
16
- "@turbo/gen": "^1.12.4",
18
+ "@turbo/gen": "^2.10.13",
17
19
  "@types/node": "^20.11.24",
18
- "@types/react": "18.3.0",
19
- "@types/react-dom": "18.3.1",
20
+ "@types/react": "19.3.0",
21
+ "@types/react-dom": "19.3.0",
20
22
  "prettier": "^3.8.3",
21
23
  "typescript": "5.5.4"
22
24
  },
package/src/Cache.js CHANGED
@@ -7,7 +7,7 @@ function getApproximateMemoryUsage(obj) {
7
7
  const seen = new WeakSet();
8
8
 
9
9
  function sizeOf(value) {
10
- // 1. Handle null and undefined explicitly (This fixes your error!)
10
+ // 1. Handle null and undefined explicitly
11
11
  if (value === null || value === undefined) return 0;
12
12
 
13
13
  // 2. Handle primitives based on JS specs
@@ -41,8 +41,6 @@ function getApproximateMemoryUsage(obj) {
41
41
 
42
42
  export default class Cache {
43
43
  constructor(opts = {}) {
44
- const self = this;
45
-
46
44
  const cacheOptions = {
47
45
  max: opts.max || 5000, // Items count
48
46
  ttl: opts.ttl || 1000 * 60 * 5, // 5 minutes
@@ -50,18 +48,18 @@ export default class Cache {
50
48
  noDeleteOnStaleGet: true,
51
49
  };
52
50
 
53
- // Conditionally use the memrory calculation since it is veery flaky and inaccurate...
51
+ // Conditionally use the memory calculation
54
52
  if (opts.maxSize) {
55
53
  cacheOptions.maxSize = opts.maxSize;
56
54
 
57
55
  cacheOptions.sizeCalculation = (value, key) => {
58
56
  return getApproximateMemoryUsage(key) + getApproximateMemoryUsage(value);
59
57
  };
60
-
61
58
  }
62
59
 
63
60
  this.debugMode = opts.debugMode || false;
64
61
  this.cacheInstance = new LRUCache(cacheOptions);
62
+ this.inFlight = new Map();
65
63
  }
66
64
 
67
65
  _debug(msg) {
@@ -82,31 +80,41 @@ export default class Cache {
82
80
  }
83
81
 
84
82
  _cloneObject(obj) {
85
- if (obj === null || obj === undefined) {
86
- return obj;
87
- }
88
-
89
- const type = typeof obj;
90
-
91
- if (type !== "function" || type !== "obj") {
83
+ if (obj === null || obj === undefined || typeof obj !== "object") {
92
84
  return obj;
93
85
  }
94
86
 
95
- // Ah the JS world! See the official docs https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy.
96
- // Important note - this works only for objects with primitive types to copy. If an object property is
97
- // a function - well it will not work correctly...
98
87
  return JSON.parse(JSON.stringify(obj));
99
88
  }
100
89
 
101
- _setInCache(key, value) {
90
+ _setInCache(key, value, opts) {
102
91
  this.cacheInstance.set(
103
92
  key,
104
- value ? this._cloneObject(value) : NULL_SENTINEL
93
+ value !== null && value !== undefined ? this._cloneObject(value) : NULL_SENTINEL,
94
+ opts
105
95
  );
106
96
  }
107
97
 
108
98
  clear() {
109
99
  this.cacheInstance.clear();
100
+ this.inFlight.clear();
101
+ }
102
+
103
+ delete(key) {
104
+ return this.cacheInstance.delete(key);
105
+ }
106
+
107
+ has(key) {
108
+ return this.cacheInstance.has(key);
109
+ }
110
+
111
+ get(key) {
112
+ const { cachedValue } = this._fetchFromCache(key);
113
+ return this._cloneObject(cachedValue);
114
+ }
115
+
116
+ set(key, value, opts) {
117
+ this._setInCache(key, value, opts);
110
118
  }
111
119
 
112
120
  forEach(callback) {
@@ -125,48 +133,77 @@ export default class Cache {
125
133
  return this.cacheInstance.maxSize;
126
134
  }
127
135
 
136
+ get inFlightCount() {
137
+ return this.inFlight.size;
138
+ }
139
+
140
+ get inFlightKeys() {
141
+ return Array.from(this.inFlight.keys());
142
+ }
143
+
144
+ async _revalidateInBackground(cacheKey, fetcher, staleValue) {
145
+ if (this.inFlight.has(cacheKey)) {
146
+ return;
147
+ }
148
+
149
+ const promise = (async () => {
150
+ try {
151
+ const freshEntity = await fetcher();
152
+ this._setInCache(cacheKey, freshEntity);
153
+ return freshEntity;
154
+ } catch (e) {
155
+ if (this.debugMode) {
156
+ console.error(`[Cache] Background revalidation failed for "${cacheKey}":`, e);
157
+ }
158
+ // Refresh TTL for the stale value so subsequent immediate requests don't hammer the backend
159
+ this._setInCache(cacheKey, staleValue);
160
+ } finally {
161
+ this.inFlight.delete(cacheKey);
162
+ }
163
+ })();
164
+
165
+ this.inFlight.set(cacheKey, promise);
166
+ }
167
+
128
168
  async fetch(cacheKey, fetcher) {
129
169
  const { cachedValue, status } = this._fetchFromCache(cacheKey);
130
170
 
171
+ // 1. Cache HIT: Return immediately
131
172
  if (status.get === "hit") {
132
173
  this._debug(`Cache HIT: ${cacheKey}`);
133
174
  return this._cloneObject(cachedValue);
134
175
  }
135
176
 
177
+ // 2. Cache STALE: Return stale value immediately and revalidate in background
136
178
  if (status.get === "stale") {
137
- this._debug(`Cache STALE: ${cacheKey}`);
179
+ this._debug(`Cache STALE (serving stale, revalidating in background): ${cacheKey}`);
180
+ this._revalidateInBackground(cacheKey, fetcher, cachedValue);
181
+ return this._cloneObject(cachedValue);
182
+ }
138
183
 
184
+ // 3. Cache MISS: Singleflight deduplication for concurrent requests
185
+ this._debug(`Cache MISS: ${cacheKey}`);
186
+ if (this.inFlight.has(cacheKey)) {
187
+ this._debug(`Cache MISS (joining in-flight fetch): ${cacheKey}`);
188
+ return this.inFlight.get(cacheKey);
189
+ }
190
+
191
+ const promise = (async () => {
139
192
  try {
140
193
  const freshEntity = await fetcher();
141
-
142
194
  this._setInCache(cacheKey, freshEntity);
143
-
144
195
  return this._cloneObject(freshEntity);
145
196
  } catch (e) {
146
- console.error(e);
147
-
148
- // Reset TTL
149
- this._setInCache(cacheKey, cachedValue);
150
-
151
- return this._cloneObject(cachedValue);
197
+ if (this.debugMode) {
198
+ console.error(`[Cache] Fetch failed for "${cacheKey}":`, e);
199
+ }
200
+ throw e;
201
+ } finally {
202
+ this.inFlight.delete(cacheKey);
152
203
  }
153
- }
154
-
155
- try {
156
- this._debug(`Cache MISS: ${cacheKey}`);
157
-
158
- const freshEntity = await fetcher();
204
+ })();
159
205
 
160
- this._setInCache(cacheKey, freshEntity);
161
-
162
- return this._cloneObject(freshEntity);
163
- } catch (e) {
164
- console.error(e);
165
-
166
- // Reset TTL
167
- this._setInCache(cacheKey, cachedValue);
168
-
169
- return this._cloneObject(cachedValue);
170
- }
206
+ this.inFlight.set(cacheKey, promise);
207
+ return promise;
171
208
  }
172
209
  }
@@ -0,0 +1,300 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import Cache from "../src/Cache.js";
4
+
5
+ describe("Cache", () => {
6
+ describe("Basic Caching (Miss & Hit)", () => {
7
+ it("should fetch and store value on cache miss, then return from cache on hit", async () => {
8
+ const cache = new Cache({ ttl: 100, debugMode: false });
9
+ let fetchCount = 0;
10
+ const fetcher = async () => {
11
+ fetchCount++;
12
+ return { id: 1, title: "Homepage", metadata: { published: true } };
13
+ };
14
+
15
+ // 1. Cache Miss
16
+ const result1 = await cache.fetch("page-home", fetcher);
17
+ assert.deepEqual(result1, { id: 1, title: "Homepage", metadata: { published: true } });
18
+ assert.equal(fetchCount, 1);
19
+
20
+ // 2. Cache Hit
21
+ const result2 = await cache.fetch("page-home", fetcher);
22
+ assert.deepEqual(result2, { id: 1, title: "Homepage", metadata: { published: true } });
23
+ assert.equal(fetchCount, 1, "Fetcher must not be called again on cache hit");
24
+ });
25
+
26
+ it("should correctly handle primitive, null, and falsy values", async () => {
27
+ const cache = new Cache({ ttl: 100, debugMode: false });
28
+
29
+ // Null value
30
+ let nullFetches = 0;
31
+ const nullFetcher = async () => {
32
+ nullFetches++;
33
+ return null;
34
+ };
35
+ assert.equal(await cache.fetch("null-val", nullFetcher), null);
36
+ assert.equal(await cache.fetch("null-val", nullFetcher), null);
37
+ assert.equal(nullFetches, 1);
38
+
39
+ // Boolean false
40
+ let boolFetches = 0;
41
+ const boolFetcher = async () => {
42
+ boolFetches++;
43
+ return false;
44
+ };
45
+ assert.equal(await cache.fetch("bool-val", boolFetcher), false);
46
+ assert.equal(await cache.fetch("bool-val", boolFetcher), false);
47
+ assert.equal(boolFetches, 1);
48
+
49
+ // Number 0
50
+ let numFetches = 0;
51
+ const numFetcher = async () => {
52
+ numFetches++;
53
+ return 0;
54
+ };
55
+ assert.equal(await cache.fetch("num-val", numFetcher), 0);
56
+ assert.equal(await cache.fetch("num-val", numFetcher), 0);
57
+ assert.equal(numFetches, 1);
58
+
59
+ // Empty string
60
+ let strFetches = 0;
61
+ const strFetcher = async () => {
62
+ strFetches++;
63
+ return "";
64
+ };
65
+ assert.equal(await cache.fetch("str-val", strFetcher), "");
66
+ assert.equal(await cache.fetch("str-val", strFetcher), "");
67
+ assert.equal(strFetches, 1);
68
+ });
69
+ });
70
+
71
+ describe("Singleflight / In-Flight Request Deduplication", () => {
72
+ it("should deduplicate concurrent requests on cache miss to a single backend call", async () => {
73
+ const cache = new Cache({ ttl: 100, debugMode: false });
74
+ let backendCalls = 0;
75
+ const slowFetcher = async () => {
76
+ backendCalls++;
77
+ await new Promise((resolve) => setTimeout(resolve, 50));
78
+ return { data: "singleflight-result", timestamp: Date.now() };
79
+ };
80
+
81
+ // 10 concurrent requests for the same uncached key
82
+ const requests = Array.from({ length: 10 }, () =>
83
+ cache.fetch("concurrent-miss-key", slowFetcher)
84
+ );
85
+
86
+ const results = await Promise.all(requests);
87
+
88
+ // Exactly 1 backend call made
89
+ assert.equal(backendCalls, 1);
90
+ assert.equal(results.length, 10);
91
+
92
+ // All callers receive identical data
93
+ const firstTimestamp = results[0].timestamp;
94
+ for (const r of results) {
95
+ assert.equal(r.data, "singleflight-result");
96
+ assert.equal(r.timestamp, firstTimestamp);
97
+ }
98
+
99
+ // In-flight map should be empty after completion
100
+ assert.equal(cache.inFlightCount, 0);
101
+ });
102
+
103
+ it("should propagate errors to all waiting concurrent callers on cache miss", async () => {
104
+ const cache = new Cache({ ttl: 100, debugMode: false });
105
+ let backendCalls = 0;
106
+ const failingFetcher = async () => {
107
+ backendCalls++;
108
+ await new Promise((resolve) => setTimeout(resolve, 30));
109
+ throw new Error("Upstream Network Timeout");
110
+ };
111
+
112
+ // 5 concurrent requests expecting failure
113
+ const requests = Array.from({ length: 5 }, () =>
114
+ cache.fetch("failing-key", failingFetcher)
115
+ );
116
+
117
+ const results = await Promise.allSettled(requests);
118
+
119
+ assert.equal(backendCalls, 1);
120
+ for (const res of results) {
121
+ assert.equal(res.status, "rejected");
122
+ assert.equal(res.reason.message, "Upstream Network Timeout");
123
+ }
124
+
125
+ // In-flight map is properly cleaned up after failure
126
+ assert.equal(cache.inFlightCount, 0);
127
+
128
+ // Subsequent attempt can retry
129
+ const successFetcher = async () => ({ recovered: true });
130
+ const recovery = await cache.fetch("failing-key", successFetcher);
131
+ assert.deepEqual(recovery, { recovered: true });
132
+ });
133
+ });
134
+
135
+ describe("Stale-While-Revalidate (SWR)", () => {
136
+ it("should return stale value instantly without waiting for revalidation", async () => {
137
+ const cache = new Cache({ ttl: 100, debugMode: false });
138
+ let fetchCount = 0;
139
+ const initialFetcher = async () => {
140
+ fetchCount++;
141
+ return { version: 1, payload: "initial" };
142
+ };
143
+
144
+ // Initial populate
145
+ await cache.fetch("swr-instant-key", initialFetcher);
146
+ assert.equal(fetchCount, 1);
147
+
148
+ // Wait 120ms (TTL is 100ms) to become stale
149
+ await new Promise((resolve) => setTimeout(resolve, 120));
150
+
151
+ let backgroundRan = false;
152
+ const slowRevalidator = async () => {
153
+ backgroundRan = true;
154
+ fetchCount++;
155
+ await new Promise((resolve) => setTimeout(resolve, 80));
156
+ return { version: 2, payload: "fresh" };
157
+ };
158
+
159
+ // Fetch on stale: must return version 1 IMMEDIATELY (< 15ms), not blocking on 80ms
160
+ const start = Date.now();
161
+ const staleResult = await cache.fetch("swr-instant-key", slowRevalidator);
162
+ const elapsed = Date.now() - start;
163
+
164
+ assert.ok(elapsed < 20, `Stale return took ${elapsed}ms; expected < 20ms`);
165
+ assert.deepEqual(staleResult, { version: 1, payload: "initial" });
166
+
167
+ // Wait 100ms for background revalidation to complete
168
+ await new Promise((resolve) => setTimeout(resolve, 100));
169
+ assert.ok(backgroundRan, "Background revalidator must have executed");
170
+ assert.equal(fetchCount, 2);
171
+
172
+ // Next request should now get the freshly revalidated data (version 2)
173
+ const freshResult = await cache.fetch("swr-instant-key", initialFetcher);
174
+ assert.deepEqual(freshResult, { version: 2, payload: "fresh" });
175
+ assert.equal(fetchCount, 2, "Fresh hit should not invoke fetcher");
176
+ });
177
+
178
+ it("should deduplicate multiple concurrent requests on a stale entry to 1 background revalidation", async () => {
179
+ const cache = new Cache({ ttl: 100, debugMode: false });
180
+ let revalidateCount = 0;
181
+ await cache.fetch("swr-concurrent-key", async () => ({ v: "v1" }));
182
+
183
+ // Wait for key to become stale
184
+ await new Promise((resolve) => setTimeout(resolve, 120));
185
+
186
+ const slowRevalidator = async () => {
187
+ revalidateCount++;
188
+ await new Promise((resolve) => setTimeout(resolve, 60));
189
+ return { v: "v2" };
190
+ };
191
+
192
+ // 10 concurrent requests on stale key
193
+ const results = await Promise.all(
194
+ Array.from({ length: 10 }, () =>
195
+ cache.fetch("swr-concurrent-key", slowRevalidator)
196
+ )
197
+ );
198
+
199
+ // All 10 requests immediately received the stale value
200
+ for (const res of results) {
201
+ assert.deepEqual(res, { v: "v1" });
202
+ }
203
+
204
+ // Wait for background revalidation to finish
205
+ await new Promise((resolve) => setTimeout(resolve, 80));
206
+
207
+ // Exactly 1 background revalidation ran
208
+ assert.equal(revalidateCount, 1);
209
+ assert.equal(cache.inFlightCount, 0);
210
+
211
+ // Cache now has fresh value
212
+ assert.deepEqual(cache.get("swr-concurrent-key"), { v: "v2" });
213
+ });
214
+
215
+ it("should retain stale value and refresh TTL when background revalidation fails (429 resilience)", async () => {
216
+ const cache = new Cache({ ttl: 100, debugMode: false });
217
+ await cache.fetch("swr-429-key", async () => ({ status: "good-data" }));
218
+
219
+ // Wait to become stale
220
+ await new Promise((resolve) => setTimeout(resolve, 120));
221
+
222
+ // Background revalidation throws upstream 429
223
+ const failingRevalidator = async () => {
224
+ throw new Error("GraphQL Error (Code: 429): Too Many Requests");
225
+ };
226
+
227
+ // Caller receives stale data safely without throwing
228
+ const staleResult = await cache.fetch("swr-429-key", failingRevalidator);
229
+ assert.deepEqual(staleResult, { status: "good-data" });
230
+
231
+ // Wait for background promise to complete error handling
232
+ await new Promise((resolve) => setTimeout(resolve, 30));
233
+
234
+ // Cache still preserves the stale data and cleared inFlight
235
+ assert.deepEqual(cache.get("swr-429-key"), { status: "good-data" });
236
+ assert.equal(cache.inFlightCount, 0);
237
+ assert.ok(cache.getRemainingTTL("swr-429-key") > 0, "TTL should be refreshed for stale entry");
238
+ });
239
+ });
240
+
241
+ describe("Lifecycle & Utility Methods", () => {
242
+ it("should support get, set, delete, has, and clear", () => {
243
+ const cache = new Cache({ ttl: 100, debugMode: false });
244
+
245
+ cache.set("item-1", { a: 1 });
246
+ assert.ok(cache.has("item-1"));
247
+ assert.deepEqual(cache.get("item-1"), { a: 1 });
248
+
249
+ cache.delete("item-1");
250
+ assert.ok(!cache.has("item-1"));
251
+ assert.equal(cache.get("item-1"), undefined);
252
+
253
+ cache.set("a", 1);
254
+ cache.set("b", 2);
255
+ cache.set("c", 3);
256
+ assert.equal(cache.has("a"), true);
257
+ assert.equal(cache.has("b"), true);
258
+
259
+ cache.clear();
260
+ assert.equal(cache.has("a"), false);
261
+ assert.equal(cache.has("b"), false);
262
+ assert.equal(cache.has("c"), false);
263
+ assert.equal(cache.inFlightCount, 0);
264
+ });
265
+
266
+ it("should support forEach iteration and TTL checking", () => {
267
+ const cache = new Cache({ ttl: 100, debugMode: false });
268
+
269
+ cache.set("alpha", { value: 10 });
270
+ cache.set("beta", { value: 20 });
271
+
272
+ const keys = [];
273
+ cache.forEach((_val, key) => {
274
+ keys.push(key);
275
+ });
276
+
277
+ assert.ok(keys.includes("alpha"));
278
+ assert.ok(keys.includes("beta"));
279
+
280
+ const ttl = cache.getRemainingTTL("alpha");
281
+ assert.ok(typeof ttl === "number" && ttl > 0);
282
+ });
283
+
284
+ it("should calculate approximate memory size when maxSize is configured", () => {
285
+ const sizedCache = new Cache({
286
+ maxSize: 10 * 1024 * 1024, // 10MB
287
+ ttl: 5000,
288
+ });
289
+
290
+ sizedCache.set("large-obj", {
291
+ title: "Test",
292
+ items: [1, 2, 3, 4, 5],
293
+ nested: { active: true },
294
+ });
295
+
296
+ assert.ok(sizedCache.calculatedSize > 0, "calculatedSize must be greater than 0");
297
+ assert.equal(sizedCache.maxSize, 10 * 1024 * 1024);
298
+ });
299
+ });
300
+ });
package/tsconfig.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "extends": "@repo/typescript-config/react-library.json",
3
3
  "compilerOptions": {
4
- "outDir": "dist"
4
+ "outDir": "dist",
5
+ "allowJs": true
5
6
  },
6
7
  "include": ["src"],
7
8
  "exclude": ["node_modules", "dist"]