@neezco/cache 0.1.1 → 0.2.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.
@@ -45,9 +45,15 @@ interface CacheConfigBase {
45
45
  /**
46
46
  * Maximum number of entries the cache can hold.
47
47
  * Beyond this limit, new entries are ignored.
48
- * @default null (unlimited)
48
+ * @default Infinite (unlimited)
49
49
  */
50
- maxSize?: number;
50
+ maxSize: number;
51
+ /**
52
+ * Maximum memory size in MB the cache can use.
53
+ * Beyond this limit, new entries are ignored.
54
+ * @default Infinite (unlimited)
55
+ */
56
+ maxMemorySize: number;
51
57
  /**
52
58
  * Controls how stale entries are handled when read from the cache.
53
59
  *
@@ -177,14 +183,19 @@ declare class LocalTtlCache {
177
183
  * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
178
184
  * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
179
185
  * @param options.tags - One or more tags for group invalidation
186
+ * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
180
187
  *
181
188
  * @example
182
189
  * ```typescript
183
- * cache.set("user:123", { name: "Alice" }, {
190
+ * const success = cache.set("user:123", { name: "Alice" }, {
184
191
  * ttl: 5 * 60 * 1000,
185
192
  * staleWindow: 1 * 60 * 1000,
186
193
  * tags: "user:123",
187
194
  * });
195
+ *
196
+ * if (!success) {
197
+ * console.log("Entry was rejected due to size or memory limits");
198
+ * }
188
199
  * ```
189
200
  *
190
201
  * @edge-cases
@@ -192,12 +203,16 @@ declare class LocalTtlCache {
192
203
  * - If `ttl` is 0 or Infinite, the entry never expires
193
204
  * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
194
205
  * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
206
+ * - Returns `false` if value is `undefined` (existing value remains untouched)
207
+ * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
208
+ * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
209
+ * - Updating existing keys always succeeds, even at limit
195
210
  */
196
211
  set(key: string, value: unknown, options?: {
197
212
  ttl?: number;
198
213
  staleWindow?: number;
199
214
  tags?: string | string[];
200
- }): void;
215
+ }): boolean;
201
216
  /**
202
217
  * Deletes a specific key from the cache.
203
218
  *
@@ -45,9 +45,15 @@ interface CacheConfigBase {
45
45
  /**
46
46
  * Maximum number of entries the cache can hold.
47
47
  * Beyond this limit, new entries are ignored.
48
- * @default null (unlimited)
48
+ * @default Infinite (unlimited)
49
49
  */
50
- maxSize?: number;
50
+ maxSize: number;
51
+ /**
52
+ * Maximum memory size in MB the cache can use.
53
+ * Beyond this limit, new entries are ignored.
54
+ * @default Infinite (unlimited)
55
+ */
56
+ maxMemorySize: number;
51
57
  /**
52
58
  * Controls how stale entries are handled when read from the cache.
53
59
  *
@@ -177,14 +183,19 @@ declare class LocalTtlCache {
177
183
  * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
178
184
  * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
179
185
  * @param options.tags - One or more tags for group invalidation
186
+ * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
180
187
  *
181
188
  * @example
182
189
  * ```typescript
183
- * cache.set("user:123", { name: "Alice" }, {
190
+ * const success = cache.set("user:123", { name: "Alice" }, {
184
191
  * ttl: 5 * 60 * 1000,
185
192
  * staleWindow: 1 * 60 * 1000,
186
193
  * tags: "user:123",
187
194
  * });
195
+ *
196
+ * if (!success) {
197
+ * console.log("Entry was rejected due to size or memory limits");
198
+ * }
188
199
  * ```
189
200
  *
190
201
  * @edge-cases
@@ -192,12 +203,16 @@ declare class LocalTtlCache {
192
203
  * - If `ttl` is 0 or Infinite, the entry never expires
193
204
  * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
194
205
  * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
206
+ * - Returns `false` if value is `undefined` (existing value remains untouched)
207
+ * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
208
+ * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
209
+ * - Updating existing keys always succeeds, even at limit
195
210
  */
196
211
  set(key: string, value: unknown, options?: {
197
212
  ttl?: number;
198
213
  staleWindow?: number;
199
214
  tags?: string | string[];
200
- }): void;
215
+ }): boolean;
201
216
  /**
202
217
  * Deletes a specific key from the cache.
203
218
  *
@@ -38,10 +38,16 @@ const DEFAULT_TTL = 30 * ONE_MINUTE;
38
38
  const DEFAULT_STALE_WINDOW = 0;
39
39
  /**
40
40
  * Maximum number of entries the cache can hold.
41
- * Beyond this limit, less-used entries are evicted.
41
+ * Beyond this limit, new entries are ignored.
42
42
  */
43
43
  const DEFAULT_MAX_SIZE = Infinity;
44
44
  /**
45
+ * Default maximum memory size in MB the cache can use.
46
+ * Beyond this limit, new entries are ignored.
47
+ * @default Infinite (unlimited)
48
+ */
49
+ const DEFAULT_MAX_MEMORY_SIZE = Infinity;
50
+ /**
45
51
  * ===================================================================
46
52
  * Sweep & Cleanup Operations
47
53
  * Parameters controlling how and when expired entries are removed.
@@ -560,11 +566,18 @@ function computeEntryStatus(state, entry, now) {
560
566
  * - It has not expired according to its own timestamps, and
561
567
  * - No associated tag imposes a stricter stale or expired rule.
562
568
  *
569
+ * `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
570
+ * Passing a pre-computed status avoids recalculating the entry status.
571
+ *
563
572
  * @param state - The cache state containing tag metadata.
564
- * @param entry - The cache entry being evaluated.
573
+ * @param entry - The cache entry or pre-computed status being evaluated.
574
+ * @param now - The current timestamp.
565
575
  * @returns True if the entry is fresh.
566
576
  */
567
- const isFresh = (state, entry, now) => computeEntryStatus(state, entry, now) === ENTRY_STATUS.FRESH;
577
+ const isFresh = (state, entry, now) => {
578
+ if (typeof entry === "string") return entry === ENTRY_STATUS.FRESH;
579
+ return computeEntryStatus(state, entry, now) === ENTRY_STATUS.FRESH;
580
+ };
568
581
  /**
569
582
  * Determines whether a cache entry is stale.
570
583
  *
@@ -572,11 +585,18 @@ const isFresh = (state, entry, now) => computeEntryStatus(state, entry, now) ===
572
585
  * - It has passed its TTL but is still within its stale window, or
573
586
  * - A tag imposes a stale rule that applies to this entry.
574
587
  *
588
+ * `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
589
+ * Passing a pre-computed status avoids recalculating the entry status.
590
+ *
575
591
  * @param state - The cache state containing tag metadata.
576
- * @param entry - The cache entry being evaluated.
592
+ * @param entry - The cache entry or pre-computed status being evaluated.
593
+ * @param now - The current timestamp.
577
594
  * @returns True if the entry is stale.
578
595
  */
579
- const isStale = (state, entry, now) => computeEntryStatus(state, entry, now) === ENTRY_STATUS.STALE;
596
+ const isStale = (state, entry, now) => {
597
+ if (typeof entry === "string") return entry === ENTRY_STATUS.STALE;
598
+ return computeEntryStatus(state, entry, now) === ENTRY_STATUS.STALE;
599
+ };
580
600
  /**
581
601
  * Determines whether a cache entry is expired.
582
602
  *
@@ -584,11 +604,18 @@ const isStale = (state, entry, now) => computeEntryStatus(state, entry, now) ===
584
604
  * - It has exceeded both its TTL and stale TTL, or
585
605
  * - A tag imposes an expiration rule that applies to this entry.
586
606
  *
607
+ * `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
608
+ * Passing a pre-computed status avoids recalculating the entry status.
609
+ *
587
610
  * @param state - The cache state containing tag metadata.
588
- * @param entry - The cache entry being evaluated.
611
+ * @param entry - The cache entry or pre-computed status being evaluated.
612
+ * @param now - The current timestamp.
589
613
  * @returns True if the entry is expired.
590
614
  */
591
- const isExpired = (state, entry, now) => computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
615
+ const isExpired = (state, entry, now) => {
616
+ if (typeof entry === "string") return entry === ENTRY_STATUS.EXPIRED;
617
+ return computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
618
+ };
592
619
 
593
620
  //#endregion
594
621
  //#region src/sweep/sweep-once.ts
@@ -613,10 +640,11 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
613
640
  processed += 1;
614
641
  const [key, entry] = next.value;
615
642
  const now = Date.now();
616
- if (isExpired(state, entry, now)) {
643
+ const status = computeEntryStatus(state, entry, now);
644
+ if (isExpired(state, status, now)) {
617
645
  deleteKey(state, key, DELETE_REASON.EXPIRED);
618
646
  expiredCount += 1;
619
- } else if (isStale(state, entry, now)) {
647
+ } else if (isStale(state, status, now)) {
620
648
  staleCount += 1;
621
649
  if (state.purgeStaleOnSweep) deleteKey(state, key, DELETE_REASON.STALE);
622
650
  }
@@ -776,7 +804,7 @@ let _initSweepScheduled = false;
776
804
  * @returns The initial cache state.
777
805
  */
778
806
  const createCache = (options = {}) => {
779
- const { onExpire, onDelete, defaultTtl = DEFAULT_TTL, maxSize = DEFAULT_MAX_SIZE, _maxAllowExpiredRatio = DEFAULT_MAX_EXPIRED_RATIO, defaultStaleWindow = DEFAULT_STALE_WINDOW, purgeStaleOnGet = false, purgeStaleOnSweep = false, _autoStartSweep = true } = options;
807
+ const { onExpire, onDelete, defaultTtl = DEFAULT_TTL, maxSize = DEFAULT_MAX_SIZE, maxMemorySize = DEFAULT_MAX_MEMORY_SIZE, _maxAllowExpiredRatio = DEFAULT_MAX_EXPIRED_RATIO, defaultStaleWindow = DEFAULT_STALE_WINDOW, purgeStaleOnGet = false, purgeStaleOnSweep = false, _autoStartSweep = true } = options;
780
808
  _instanceCount++;
781
809
  if (_instanceCount > INSTANCE_WARNING_THRESHOLD) console.warn(`Too many instances detected (${_instanceCount}). This may indicate a configuration issue; consider minimizing instance creation or grouping keys by expected expiration ranges. See the documentation: https://github.com/neezco/cache/docs/getting-started.md`);
782
810
  const state = {
@@ -788,6 +816,7 @@ const createCache = (options = {}) => {
788
816
  onExpire,
789
817
  onDelete,
790
818
  maxSize,
819
+ maxMemorySize,
791
820
  defaultTtl,
792
821
  defaultStaleWindow,
793
822
  purgeStaleOnGet,
@@ -821,8 +850,9 @@ const createCache = (options = {}) => {
821
850
  const get = (state, key, now = Date.now()) => {
822
851
  const entry = state.store.get(key);
823
852
  if (!entry) return void 0;
824
- if (isFresh(state, entry, now)) return entry[1];
825
- if (isStale(state, entry, now)) {
853
+ const status = computeEntryStatus(state, entry, now);
854
+ if (isFresh(state, status, now)) return entry[1];
855
+ if (isStale(state, status, now)) {
826
856
  if (state.purgeStaleOnGet) deleteKey(state, key, DELETE_REASON.STALE);
827
857
  return entry[1];
828
858
  }
@@ -888,16 +918,23 @@ function invalidateTag(state, tags, options = {}, _now = Date.now()) {
888
918
  * @param state - The cache state.
889
919
  * @param input - Cache entry definition (key, value, ttl, staleWindow, tags).
890
920
  * @param now - Optional timestamp override used as the base time (defaults to Date.now()).
921
+ * @returns True if the entry was created or updated, false if rejected due to limits or invalid input.
891
922
  *
892
923
  * @remarks
893
924
  * - `ttl` defines when the entry becomes expired.
894
925
  * - `staleWindow` defines how long the entry may still be served as stale
895
926
  * after the expiration moment (`now + ttl`).
927
+ * - Returns false if value is `undefined` (entry ignored, existing value untouched).
928
+ * - Returns false if new entry would exceed `maxSize` limit (existing keys always allowed).
929
+ * - Returns false if new entry would exceed `maxMemorySize` limit (existing keys always allowed).
930
+ * - Returns true if entry was set or updated (or if existing key was updated at limit).
896
931
  */
897
932
  const setOrUpdate = (state, input, now = Date.now()) => {
898
933
  const { key, value, ttl: ttlInput, staleWindow: staleWindowInput, tags } = input;
899
- if (value === void 0) return;
934
+ if (value === void 0) return false;
900
935
  if (key == null) throw new Error("Missing key.");
936
+ if (state.size >= state.maxSize && !state.store.has(key)) return false;
937
+ if (_metrics?.memory.total.rss && _metrics?.memory.total.rss >= state.maxMemorySize * 1024 * 1024 && !state.store.has(key)) return false;
901
938
  const ttl = ttlInput ?? state.defaultTtl;
902
939
  const staleWindow = staleWindowInput ?? state.defaultStaleWindow;
903
940
  const expiresAt = ttl > 0 ? now + ttl : Infinity;
@@ -911,6 +948,7 @@ const setOrUpdate = (state, input, now = Date.now()) => {
911
948
  typeof tags === "string" ? [tags] : Array.isArray(tags) ? tags : null
912
949
  ];
913
950
  state.store.set(key, entry);
951
+ return true;
914
952
  };
915
953
 
916
954
  //#endregion
@@ -1002,14 +1040,19 @@ var LocalTtlCache = class {
1002
1040
  * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
1003
1041
  * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
1004
1042
  * @param options.tags - One or more tags for group invalidation
1043
+ * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
1005
1044
  *
1006
1045
  * @example
1007
1046
  * ```typescript
1008
- * cache.set("user:123", { name: "Alice" }, {
1047
+ * const success = cache.set("user:123", { name: "Alice" }, {
1009
1048
  * ttl: 5 * 60 * 1000,
1010
1049
  * staleWindow: 1 * 60 * 1000,
1011
1050
  * tags: "user:123",
1012
1051
  * });
1052
+ *
1053
+ * if (!success) {
1054
+ * console.log("Entry was rejected due to size or memory limits");
1055
+ * }
1013
1056
  * ```
1014
1057
  *
1015
1058
  * @edge-cases
@@ -1017,9 +1060,13 @@ var LocalTtlCache = class {
1017
1060
  * - If `ttl` is 0 or Infinite, the entry never expires
1018
1061
  * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
1019
1062
  * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
1063
+ * - Returns `false` if value is `undefined` (existing value remains untouched)
1064
+ * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
1065
+ * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
1066
+ * - Updating existing keys always succeeds, even at limit
1020
1067
  */
1021
1068
  set(key, value, options) {
1022
- setOrUpdate(this.state, {
1069
+ return setOrUpdate(this.state, {
1023
1070
  key,
1024
1071
  value,
1025
1072
  ttl: options?.ttl,