@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.
- package/CHANGELOG.md +23 -0
- package/dist/browser/index.d.ts +19 -4
- package/dist/browser/index.js +61 -15
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +62 -15
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +19 -4
- package/dist/node/index.d.mts +19 -4
- package/dist/node/index.mjs +62 -15
- package/dist/node/index.mjs.map +1 -1
- package/docs/api-reference.md +17 -4
- package/docs/configuration.md +41 -2
- package/docs/contributing/code-style.md +40 -0
- package/docs/contributing/issues.md +30 -0
- package/docs/contributing/project-structure.md +35 -0
- package/docs/contributing/scripts.md +38 -0
- package/docs/contributing/setup.md +49 -0
- package/docs/contributing/workflow.md +71 -0
- package/package.json +3 -3
package/dist/node/index.d.cts
CHANGED
|
@@ -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
|
|
48
|
+
* @default Infinite (unlimited)
|
|
49
49
|
*/
|
|
50
|
-
maxSize
|
|
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
|
-
}):
|
|
215
|
+
}): boolean;
|
|
201
216
|
/**
|
|
202
217
|
* Deletes a specific key from the cache.
|
|
203
218
|
*
|
package/dist/node/index.d.mts
CHANGED
|
@@ -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
|
|
48
|
+
* @default Infinite (unlimited)
|
|
49
49
|
*/
|
|
50
|
-
maxSize
|
|
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
|
-
}):
|
|
215
|
+
}): boolean;
|
|
201
216
|
/**
|
|
202
217
|
* Deletes a specific key from the cache.
|
|
203
218
|
*
|
package/dist/node/index.mjs
CHANGED
|
@@ -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,
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
825
|
-
if (
|
|
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,
|