• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

flyingsquirrel0419 / layercache / 25206048603

01 May 2026 07:08AM UTC coverage: 95.607% (-0.1%) from 95.722%
25206048603

Pull #33

github

web-flow
Merge 64da5649a into 681953ebf
Pull Request #33: feat: add context-aware cache entry options

1611 of 1732 branches covered (93.01%)

Branch coverage included in aggregate %.

29 of 33 new or added lines in 2 files covered. (87.88%)

25 existing lines in 1 file now uncovered.

2916 of 3003 relevant lines covered (97.1%)

332.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.67
/src/internal/CacheStackValidation.ts
1
import type {
2
  CacheAdaptiveTtlOptions,
3
  CacheCircuitBreakerOptions,
4
  CacheEntryWriteOptions,
5
  CacheRateLimitOptions,
6
  CacheTtlPolicy,
7
  LayerTtlMap
8
} from '../types'
9

10
export const MAX_CACHE_KEY_LENGTH = 1_024
13✔
11
export const MAX_PATTERN_LENGTH = 1_024
13✔
12
export const MAX_TAGS_PER_OPERATION = 128
13✔
13

14
export function validatePositiveNumber(name: string, value: number | undefined): void {
15
  if (value === undefined) {
1,856✔
16
    return
1,802✔
17
  }
18

19
  if (!Number.isFinite(value) || value <= 0) {
54✔
20
    throw new Error(`${name} must be a positive finite number.`)
5✔
21
  }
22
}
23

24
export function validateNonNegativeNumber(name: string, value: number): void {
25
  if (!Number.isFinite(value) || value < 0) {
66✔
26
    throw new Error(`${name} must be a non-negative finite number.`)
6✔
27
  }
28
}
29

30
export function validateLayerNumberOption(name: string, value: number | LayerTtlMap | undefined): void {
31
  if (value === undefined) {
1,890✔
32
    return
1,832✔
33
  }
34

35
  if (typeof value === 'number') {
58✔
36
    validateNonNegativeNumber(name, value)
54✔
37
    return
54✔
38
  }
39

40
  for (const [layerName, layerValue] of Object.entries(value)) {
4✔
41
    if (layerValue === undefined) {
6✔
42
      continue
1✔
43
    }
44

45
    validateNonNegativeNumber(`${name}.${layerName}`, layerValue)
5✔
46
  }
47
}
48

49
export function validateRateLimitOptions(name: string, options: CacheRateLimitOptions | undefined): void {
50
  if (!options) {
353✔
51
    return
343✔
52
  }
53

54
  validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent)
10✔
55
  validatePositiveNumber(`${name}.intervalMs`, options.intervalMs)
10✔
56
  validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval)
10✔
57

58
  if (options.scope && !['global', 'key', 'fetcher'].includes(options.scope)) {
10✔
59
    throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`)
1✔
60
  }
61

62
  if (options.bucketKey !== undefined && options.bucketKey.length === 0) {
8✔
63
    throw new Error(`${name}.bucketKey must not be empty.`)
1✔
64
  }
65
}
66

67
export function validateCacheKey(key: string): string {
68
  if (key.length === 0) {
459✔
69
    throw new Error('Cache key must not be empty.')
2✔
70
  }
71

72
  if (key.length > MAX_CACHE_KEY_LENGTH) {
454✔
73
    throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`)
1✔
74
  }
75

76
  if (/[\u0000-\u001F\u007F]/.test(key)) {
453✔
77
    throw new Error('Cache key contains unsupported control characters.')
2✔
78
  }
79

80
  if (/[\uD800-\uDFFF]/.test(key)) {
451✔
81
    throw new Error('Cache key contains unsupported surrogate code points.')
1✔
82
  }
83

84
  return key
450✔
85
}
86

87
export function validateTag(tag: string): string {
88
  if (tag.length === 0) {
56✔
89
    throw new Error('Cache tag must not be empty.')
1✔
90
  }
91

92
  if (tag.length > MAX_CACHE_KEY_LENGTH) {
55✔
93
    throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`)
1✔
94
  }
95

96
  if (/[\u0000-\u001F\u007F]/.test(tag)) {
54✔
97
    throw new Error('Cache tag contains unsupported control characters.')
3✔
98
  }
99

100
  if (/[\uD800-\uDFFF]/.test(tag)) {
51✔
101
    throw new Error('Cache tag contains unsupported surrogate code points.')
1✔
102
  }
103

104
  return tag
50✔
105
}
106

107
export function validateTags(tags: string[] | undefined): void {
108
  if (!tags) {
132✔
109
    return
101✔
110
  }
111

112
  if (tags.length > MAX_TAGS_PER_OPERATION) {
31✔
113
    throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`)
1✔
114
  }
115

116
  for (const tag of tags) {
30✔
117
    validateTag(tag)
42✔
118
  }
119
}
120

121
export function validatePattern(pattern: string): void {
122
  if (pattern.length === 0) {
10✔
123
    throw new Error('Pattern must not be empty.')
1✔
124
  }
125

126
  if (pattern.length > MAX_PATTERN_LENGTH) {
9✔
127
    throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`)
1✔
128
  }
129

130
  if (/[\u0000-\u001F\u007F]/.test(pattern)) {
8✔
131
    throw new Error('Pattern contains unsupported control characters.')
1✔
132
  }
133
}
134

135
export function validateTtlPolicy(name: string, policy: CacheTtlPolicy | undefined): void {
136
  if (!policy || typeof policy === 'function' || policy === 'until-midnight' || policy === 'next-hour') {
131✔
137
    return
127✔
138
  }
139

140
  if ('alignTo' in policy) {
4✔
141
    validatePositiveNumber(`${name}.alignTo`, policy.alignTo)
3✔
142
    return
3✔
143
  }
144

145
  throw new Error(`${name} is invalid.`)
1✔
146
}
147

148
export function validateAdaptiveTtlOptions(options: boolean | CacheAdaptiveTtlOptions | undefined): void {
149
  if (!options || options === true) {
355✔
150
    return
352✔
151
  }
152

153
  validatePositiveNumber('adaptiveTtl.hotAfter', options.hotAfter)
3✔
154
  validateLayerNumberOption('adaptiveTtl.step', options.step)
3✔
155
  validateLayerNumberOption('adaptiveTtl.maxTtl', options.maxTtl)
3✔
156
}
157

158
export function validateCircuitBreakerOptions(options: CacheCircuitBreakerOptions | undefined): void {
159
  if (!options) {
351✔
160
    return
340✔
161
  }
162

163
  validatePositiveNumber('circuitBreaker.failureThreshold', options.failureThreshold)
11✔
164
  validatePositiveNumber('circuitBreaker.cooldownMs', options.cooldownMs)
11✔
165
}
166

167
export function validateContextEntryOptions(name: string, options: CacheEntryWriteOptions | undefined): void {
168
  if (!options) {
5!
NEW
169
    return
×
170
  }
171

172
  validateLayerNumberOption(`${name}.ttl`, options.ttl)
5✔
173
  validateLayerNumberOption(`${name}.negativeTtl`, options.negativeTtl)
5✔
174
  validateLayerNumberOption(`${name}.staleWhileRevalidate`, options.staleWhileRevalidate)
5✔
175
  validateLayerNumberOption(`${name}.staleIfError`, options.staleIfError)
5✔
176
  validateLayerNumberOption(`${name}.ttlJitter`, options.ttlJitter)
5✔
177
  validateTtlPolicy(`${name}.ttlPolicy`, options.ttlPolicy)
5✔
178
  validateAdaptiveTtlOptions(options.adaptiveTtl)
5✔
179
  validateTags(options.tags)
5✔
180
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc