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

flyingsquirrel0419 / layercache / 24937290633

25 Apr 2026 06:09PM UTC coverage: 95.061% (-0.3%) from 95.325%
24937290633

Pull #19

github

web-flow
Merge 048a6b31d into 5999d48b9
Pull Request #19: Security/vulnerability fixes

1594 of 1722 branches covered (92.57%)

Branch coverage included in aggregate %.

29 of 39 new or added lines in 7 files covered. (74.36%)

2852 of 2955 relevant lines covered (96.51%)

262.65 hits per line

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

89.22
/src/internal/CacheStackMaintenance.ts
1
import type { CacheWriteBehindOptions } from '../types'
2

3
type WriteBehindOperation = () => Promise<void>
4
type FlushWriteBehindBatch = (batch: WriteBehindOperation[]) => Promise<void>
5
type GenerationCleanupTask = (generation: number) => Promise<void>
6
type GenerationCleanupErrorHandler = (generation: number, error: unknown) => void
7

8
const MAX_KEY_EPOCHS = 50_000
12✔
9

10
export class CacheStackMaintenance {
11
  private readonly keyEpochs = new Map<string, number>()
229✔
12
  private readonly writeBehindQueue: WriteBehindOperation[] = []
229✔
13
  private writeBehindTimer?: ReturnType<typeof setInterval>
14
  private writeBehindFlushPromise?: Promise<void>
15
  private generationCleanupPromise?: Promise<void>
16
  private clearEpoch = 0
229✔
17

18
  initializeWriteBehindTimer(
19
    writeStrategy: 'write-through' | 'write-behind' | undefined,
20
    options: CacheWriteBehindOptions | undefined,
21
    flush: () => Promise<void>
22
  ): void {
23
    if (writeStrategy !== 'write-behind') {
221✔
24
      return
214✔
25
    }
26

27
    const flushIntervalMs = options?.flushIntervalMs
7✔
28
    if (!flushIntervalMs || flushIntervalMs <= 0) {
221✔
29
      return
5✔
30
    }
31

32
    this.disposeWriteBehindTimer()
2✔
33
    this.writeBehindTimer = setInterval(() => {
2✔
34
      void flush()
1✔
35
    }, flushIntervalMs)
36
    this.writeBehindTimer.unref?.()
2✔
37
  }
38

39
  disposeWriteBehindTimer(): void {
40
    if (!this.writeBehindTimer) {
29✔
41
      return
27✔
42
    }
43

44
    clearInterval(this.writeBehindTimer)
2✔
45
    this.writeBehindTimer = undefined
2✔
46
  }
47

48
  beginClearEpoch(): void {
49
    this.clearEpoch += 1
7✔
50
    this.keyEpochs.clear()
7✔
51
    this.writeBehindQueue.length = 0
7✔
52
  }
53

54
  currentClearEpoch(): number {
55
    return this.clearEpoch
323✔
56
  }
57

58
  currentKeyEpoch(key: string): number {
59
    return this.keyEpochs.get(key) ?? 0
699✔
60
  }
61

62
  bumpKeyEpochs(keys: string[]): void {
63
    for (const key of keys) {
30✔
64
      this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1)
38✔
65
    }
66
    this.pruneKeyEpochsIfNeeded()
30✔
67
  }
68

69
  isWriteOutdated(key: string, expectedClearEpoch?: number, expectedKeyEpoch?: number): boolean {
70
    if (expectedClearEpoch !== undefined && expectedClearEpoch !== this.clearEpoch) {
370✔
71
      return true
2✔
72
    }
73

74
    if (expectedKeyEpoch !== undefined && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
368✔
75
      return true
3✔
76
    }
77

78
    return false
365✔
79
  }
80

81
  async enqueueWriteBehind(
82
    operation: WriteBehindOperation,
83
    options: CacheWriteBehindOptions | undefined,
84
    flushBatch: FlushWriteBehindBatch
85
  ): Promise<void> {
86
    this.writeBehindQueue.push(operation)
15✔
87
    const batchSize = options?.batchSize ?? 100
15!
88
    const maxQueueSize = options?.maxQueueSize ?? batchSize * 10
15✔
89

90
    if (this.writeBehindQueue.length >= batchSize) {
15✔
91
      await this.flushWriteBehindQueue(options, flushBatch)
5✔
92
      return
5✔
93
    }
94

95
    if (this.writeBehindQueue.length >= maxQueueSize) {
10✔
96
      await this.flushWriteBehindQueue(options, flushBatch)
1✔
97
    }
98
  }
99

100
  async flushWriteBehindQueue(
101
    options: CacheWriteBehindOptions | undefined,
102
    flushBatch: FlushWriteBehindBatch
103
  ): Promise<void> {
104
    if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
36✔
105
      await this.writeBehindFlushPromise
28✔
106
      return
28✔
107
    }
108

109
    const batchSize = options?.batchSize ?? 100
8!
110
    const batch = this.writeBehindQueue.splice(0, batchSize)
36✔
111
    this.writeBehindFlushPromise = flushBatch(batch)
36✔
112

113
    try {
36✔
114
      await this.writeBehindFlushPromise
36✔
115
    } finally {
116
      this.writeBehindFlushPromise = undefined
8✔
117
    }
118

119
    if (this.writeBehindQueue.length > 0) {
8!
120
      await this.flushWriteBehindQueue(options, flushBatch)
×
121
    }
122
  }
123

124
  scheduleGenerationCleanup(
125
    generation: number,
126
    task: GenerationCleanupTask,
127
    onError: GenerationCleanupErrorHandler
128
  ): void {
129
    const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve())
6✔
130
      .then(() => task(generation))
6✔
131
      .catch((error) => {
132
        onError(generation, error)
2✔
133
      })
134

135
    this.generationCleanupPromise = scheduledTask.finally(() => {
6✔
136
      if (this.generationCleanupPromise === scheduledTask) {
6!
137
        this.generationCleanupPromise = undefined
×
138
      }
139
    })
140
  }
141

142
  async waitForGenerationCleanup(): Promise<void> {
143
    await this.generationCleanupPromise
28✔
144
  }
145

146
  private pruneKeyEpochsIfNeeded(): void {
147
    if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
30!
148
      return
30✔
149
    }
150

NEW
151
    const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1])
×
NEW
152
    const toDelete = Math.ceil(sorted.length * 0.1)
×
NEW
153
    for (let i = 0; i < toDelete; i++) {
×
NEW
154
      this.keyEpochs.delete(sorted[i][0])
×
155
    }
156
  }
157
}
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