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

flyingsquirrel0419 / layercache / 29149114635

11 Jul 2026 10:17AM UTC coverage: 95.423% (-0.5%) from 95.882%
29149114635

Pull #101

github

web-flow
Merge 3a92bb146 into e5107fec2
Pull Request #101: Fix cache security hardening

1951 of 2112 branches covered (92.38%)

Branch coverage included in aggregate %.

287 of 307 new or added lines in 23 files covered. (93.49%)

5 existing lines in 2 files now uncovered.

3428 of 3525 relevant lines covered (97.25%)

302.46 hits per line

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

91.67
/src/internal/CacheStackLayerWriter.ts
1
import type { CacheLayer, CacheLayerSetManyEntry, CacheWriteOptions, LayerTtlMap } from '../types'
2
import type { CacheStackMaintenance } from './CacheStackMaintenance'
3
import { createStoredValueEnvelope, remainingStoredTtlMs } from './StoredValue'
4

5
export type CacheWriteKind = 'value' | 'empty'
6

7
interface CacheStackLayerWriterOptions {
8
  layers: CacheLayer[]
9
  maintenance: CacheStackMaintenance
10
  shouldSkipLayer: (layer: CacheLayer) => boolean
11
  shouldWriteBehind: (layer: CacheLayer) => boolean
12
  handleLayerFailure: (layer: CacheLayer, operation: string, error: unknown) => Promise<void>
13
  enqueueWriteBehind: (operation: () => Promise<void>) => Promise<void>
14
  resolveFreshTtl: (
15
    key: string,
16
    layerName: string,
17
    kind: CacheWriteKind,
18
    options: CacheWriteOptions | undefined,
19
    fallbackTtl: number | undefined,
20
    value: unknown
21
  ) => number | undefined
22
  resolveLayerMs: (
23
    layerName: string,
24
    override: number | LayerTtlMap | undefined,
25
    globalDefault?: number | LayerTtlMap,
26
    fallback?: number
27
  ) => number | undefined
28
  globalStaleWhileRevalidate: number | LayerTtlMap | undefined
29
  globalStaleIfError: number | LayerTtlMap | undefined
30
  writePolicy: 'strict' | 'best-effort' | undefined
31
  onWriteFailures: (context: { key: string; action: string }, failures: unknown[]) => void
32
}
33

34
interface LayerBatchEntry {
35
  key: string
36
  value: unknown
37
  options?: CacheWriteOptions
38
}
39

40
export interface CacheWriteFence {
41
  clearEpoch: number
42
  keyEpoch: number
43
}
44

45
export class CacheStackLayerWriter {
46
  constructor(private readonly options: CacheStackLayerWriterOptions) {}
276✔
47

48
  async writeAcrossLayers(
49
    key: string,
50
    kind: CacheWriteKind,
51
    value: unknown,
52
    writeOptions?: CacheWriteOptions,
53
    fence?: CacheWriteFence
54
  ): Promise<boolean> {
55
    const now = Date.now()
186✔
56
    const clearEpoch = fence?.clearEpoch ?? this.options.maintenance.currentClearEpoch()
186✔
57
    const keyEpoch = fence?.keyEpoch ?? this.options.maintenance.currentKeyEpoch(key)
186✔
58
    const immediateOperations: Array<() => Promise<void>> = []
186✔
59
    const deferredOperations: Array<() => Promise<void>> = []
186✔
60

61
    for (const layer of this.options.layers) {
186✔
62
      const operation = async () => {
212✔
63
        if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
211!
64
          return
×
65
        }
66
        if (this.options.shouldSkipLayer(layer)) {
211✔
67
          return
1✔
68
        }
69

70
        const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now)
210✔
71
        try {
210✔
72
          await layer.set(entry.key, entry.value, entry.ttl)
210✔
73
        } catch (error) {
74
          await this.options.handleLayerFailure(layer, 'write', error)
6✔
75
        }
76
      }
77

78
      if (this.options.shouldWriteBehind(layer)) {
212✔
79
        deferredOperations.push(operation)
3✔
80
      } else {
81
        immediateOperations.push(operation)
209✔
82
      }
83
    }
84

85
    const immediateLayers = this.options.layers.filter(
186✔
86
      (layer) => !this.options.shouldSkipLayer(layer) && !this.options.shouldWriteBehind(layer)
212✔
87
    )
88
    const committed = await this.options.maintenance.runFencedWrite(
186✔
89
      key,
90
      clearEpoch,
91
      keyEpoch,
92
      () =>
93
        this.executeLayerOperations(immediateOperations, { key, action: kind === 'empty' ? 'negative-set' : 'set' }),
186✔
94
      () => this.deleteFromLayers(key, immediateLayers)
1✔
95
    )
96
    await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)))
184✔
97
    return committed
184✔
98
  }
99

100
  private async deleteFromLayers(key: string, layers: CacheLayer[]): Promise<void> {
101
    await Promise.all(
1✔
102
      layers.map(async (layer) => {
103
        try {
1✔
104
          await layer.delete(key)
1✔
105
        } catch (error) {
NEW
106
          await this.options.handleLayerFailure(layer, 'stale-write-cleanup', error)
×
107
        }
108
      })
109
    )
110
  }
111

112
  async writeBatch(entries: LayerBatchEntry[]): Promise<{ clearEpoch: number; entryEpochs: Map<string, number> }> {
113
    const now = Date.now()
18✔
114
    const clearEpoch = this.options.maintenance.currentClearEpoch()
18✔
115
    const entryEpochs = new Map(
18✔
116
      entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
38✔
117
    )
118
    const entriesByLayer = new Map<CacheLayer, CacheLayerSetManyEntry[]>()
18✔
119
    const immediateOperations: Array<() => Promise<void>> = []
18✔
120
    const deferredOperations: Array<() => Promise<void>> = []
18✔
121

122
    for (const entry of entries) {
18✔
123
      for (const layer of this.options.layers) {
38✔
124
        if (this.options.shouldSkipLayer(layer)) {
40!
125
          continue
×
126
        }
127

128
        const layerEntry = this.buildLayerSetEntry(layer, entry.key, 'value', entry.value, entry.options, now)
40✔
129
        const bucket = entriesByLayer.get(layer) ?? []
40✔
130
        bucket.push(layerEntry)
40✔
131
        entriesByLayer.set(layer, bucket)
40✔
132
      }
133
    }
134

135
    for (const [layer, layerEntries] of entriesByLayer.entries()) {
18✔
136
      const operation = async () => {
20✔
137
        if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
20!
138
          return
×
139
        }
140
        const activeEntries = layerEntries.filter(
20✔
141
          (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
40!
142
        )
143
        if (activeEntries.length === 0) {
20✔
144
          return
1✔
145
        }
146
        try {
19✔
147
          if (layer.setMany) {
19✔
148
            await layer.setMany(activeEntries)
12✔
149
            return
12✔
150
          }
151

152
          await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)))
9✔
153
        } catch (error) {
154
          await this.options.handleLayerFailure(layer, 'write', error)
2✔
155
        }
156
      }
157

158
      if (this.options.shouldWriteBehind(layer)) {
20✔
159
        deferredOperations.push(operation)
2✔
160
      } else {
161
        immediateOperations.push(operation)
18✔
162
      }
163
    }
164

165
    await this.executeLayerOperations(immediateOperations, { key: 'batch', action: 'mset' })
18✔
166
    await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)))
17✔
167
    return { clearEpoch, entryEpochs }
17✔
168
  }
169

170
  private async executeLayerOperations(
171
    operations: Array<() => Promise<void>>,
172
    context: { key: string; action: string }
173
  ): Promise<void> {
174
    if (this.options.writePolicy !== 'best-effort') {
204✔
175
      await Promise.all(operations.map((operation) => operation()))
218✔
176
      return
198✔
177
    }
178

179
    const results = await Promise.allSettled(operations.map((operation) => operation()))
9✔
180
    const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
9✔
181
    const degraded = results.filter((result): result is PromiseFulfilledResult<void> => result.status === 'fulfilled')
9✔
182
    if (failures.length === 0) {
5!
183
      return
×
184
    }
185

186
    this.options.onWriteFailures(
5✔
187
      context,
188
      failures.map((failure) => failure.reason)
6✔
189
    )
190

191
    // Throw when every layer either rejected or was fulfilled via graceful degradation
192
    // (handleLayerFailure returns null without re-throwing). Both paths indicate actual failure.
193
    if (failures.length === operations.length) {
5✔
194
      throw new AggregateError(
2✔
195
        failures.map((failure) => failure.reason),
3✔
196
        `${context.action} failed for every cache layer`
197
      )
198
    }
199
  }
200

201
  private buildLayerSetEntry(
202
    layer: CacheLayer,
203
    key: string,
204
    kind: CacheWriteKind,
205
    value: unknown,
206
    writeOptions: CacheWriteOptions | undefined,
207
    now: number
208
  ): CacheLayerSetManyEntry {
209
    const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value)
250✔
210
    const staleWhileRevalidate = this.options.resolveLayerMs(
250✔
211
      layer.name,
212
      writeOptions?.staleWhileRevalidate,
213
      this.options.globalStaleWhileRevalidate
214
    )
215
    const staleIfError = this.options.resolveLayerMs(
250✔
216
      layer.name,
217
      writeOptions?.staleIfError,
218
      this.options.globalStaleIfError
219
    )
220
    const payload = createStoredValueEnvelope({
250✔
221
      kind,
222
      value,
223
      freshTtlMs: freshTtl,
224
      staleWhileRevalidateMs: staleWhileRevalidate,
225
      staleIfErrorMs: staleIfError,
226
      now
227
    })
228
    const ttl = remainingStoredTtlMs(payload, now) ?? freshTtl
250✔
229
    return {
250✔
230
      key,
231
      value: payload,
232
      ttl
233
    }
234
  }
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc