Skip to content

Commit 5453139

Browse files
authored
refactor: snapshot-recorder (#4413)
* refactor: snapshot-recorder * improve further * simplify matchHeaders * remove unnecessary case * simplify * improve * reuse sets * simplify * improve * more jsdoc * fix potential race condition * one last flush
1 parent 16e7812 commit 5453139

5 files changed

Lines changed: 454 additions & 234 deletions

File tree

lib/mock/snapshot-agent.js

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ const MockAgent = require('./mock-agent')
55
const { SnapshotRecorder } = require('./snapshot-recorder')
66
const WrapHandler = require('../handler/wrap-handler')
77
const { InvalidArgumentError, UndiciError } = require('../core/errors')
8+
const { validateSnapshotMode } = require('./snapshot-utils')
89

910
const kSnapshotRecorder = Symbol('kSnapshotRecorder')
1011
const kSnapshotMode = Symbol('kSnapshotMode')
1112
const kSnapshotPath = Symbol('kSnapshotPath')
1213
const kSnapshotLoaded = Symbol('kSnapshotLoaded')
1314
const kRealAgent = Symbol('kRealAgent')
1415

15-
// Static flag to ensure warning is only emitted once
16+
// Static flag to ensure warning is only emitted once per process
1617
let warningEmitted = false
1718

1819
class SnapshotAgent extends MockAgent {
@@ -26,26 +27,24 @@ class SnapshotAgent extends MockAgent {
2627
warningEmitted = true
2728
}
2829

29-
const mockOptions = { ...opts }
30-
delete mockOptions.mode
31-
delete mockOptions.snapshotPath
30+
const {
31+
mode = 'record',
32+
snapshotPath = null,
33+
...mockAgentOpts
34+
} = opts
3235

33-
super(mockOptions)
36+
super(mockAgentOpts)
3437

35-
// Validate mode option
36-
const validModes = ['record', 'playback', 'update']
37-
const mode = opts.mode || 'record'
38-
if (!validModes.includes(mode)) {
39-
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validModes.join(', ')}`)
40-
}
38+
validateSnapshotMode(mode)
4139

4240
// Validate snapshotPath is provided when required
43-
if ((mode === 'playback' || mode === 'update') && !opts.snapshotPath) {
41+
if ((mode === 'playback' || mode === 'update') && !snapshotPath) {
4442
throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)
4543
}
4644

4745
this[kSnapshotMode] = mode
48-
this[kSnapshotPath] = opts.snapshotPath
46+
this[kSnapshotPath] = snapshotPath
47+
4948
this[kSnapshotRecorder] = new SnapshotRecorder({
5049
snapshotPath: this[kSnapshotPath],
5150
mode: this[kSnapshotMode],
@@ -85,18 +84,18 @@ class SnapshotAgent extends MockAgent {
8584
// Ensure snapshots are loaded
8685
if (!this[kSnapshotLoaded]) {
8786
// Need to load asynchronously, delegate to async version
88-
return this._asyncDispatch(opts, handler)
87+
return this.#asyncDispatch(opts, handler)
8988
}
9089

9190
// Try to find existing snapshot (synchronous)
9291
const snapshot = this[kSnapshotRecorder].findSnapshot(opts)
9392

9493
if (snapshot) {
9594
// Use recorded response (synchronous)
96-
return this._replaySnapshot(snapshot, handler)
95+
return this.#replaySnapshot(snapshot, handler)
9796
} else if (mode === 'update') {
9897
// Make real request and record it (async required)
99-
return this._recordAndReplay(opts, handler)
98+
return this.#recordAndReplay(opts, handler)
10099
} else {
101100
// Playback mode but no snapshot found
102101
const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)
@@ -108,24 +107,22 @@ class SnapshotAgent extends MockAgent {
108107
}
109108
} else if (mode === 'record') {
110109
// Record mode - make real request and save response (async required)
111-
return this._recordAndReplay(opts, handler)
112-
} else {
113-
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be 'record', 'playback', or 'update'`)
110+
return this.#recordAndReplay(opts, handler)
114111
}
115112
}
116113

117114
/**
118115
* Async version of dispatch for when we need to load snapshots first
119116
*/
120-
async _asyncDispatch (opts, handler) {
117+
async #asyncDispatch (opts, handler) {
121118
await this.loadSnapshots()
122119
return this.dispatch(opts, handler)
123120
}
124121

125122
/**
126123
* Records a real request and replays the response
127124
*/
128-
_recordAndReplay (opts, handler) {
125+
#recordAndReplay (opts, handler) {
129126
const responseData = {
130127
statusCode: null,
131128
headers: {},
@@ -180,8 +177,12 @@ class SnapshotAgent extends MockAgent {
180177

181178
/**
182179
* Replays a recorded response
180+
*
181+
* @param {Object} snapshot - The recorded snapshot to replay.
182+
* @param {Object} handler - The handler to call with the response data.
183+
* @returns {void}
183184
*/
184-
_replaySnapshot (snapshot, handler) {
185+
#replaySnapshot (snapshot, handler) {
185186
try {
186187
const { response } = snapshot
187188

@@ -213,19 +214,25 @@ class SnapshotAgent extends MockAgent {
213214

214215
/**
215216
* Loads snapshots from file
217+
*
218+
* @param {string} [filePath] - Optional file path to load snapshots from.
219+
* @returns {Promise<void>} - Resolves when snapshots are loaded.
216220
*/
217221
async loadSnapshots (filePath) {
218222
await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])
219223
this[kSnapshotLoaded] = true
220224

221225
// In playback mode, set up MockAgent interceptors for all snapshots
222226
if (this[kSnapshotMode] === 'playback') {
223-
this._setupMockInterceptors()
227+
this.#setupMockInterceptors()
224228
}
225229
}
226230

227231
/**
228232
* Saves snapshots to file
233+
*
234+
* @param {string} [filePath] - Optional file path to save snapshots to.
235+
* @returns {Promise<void>} - Resolves when snapshots are saved.
229236
*/
230237
async saveSnapshots (filePath) {
231238
return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])
@@ -242,9 +249,9 @@ class SnapshotAgent extends MockAgent {
242249
*
243250
* Called automatically when loading snapshots in playback mode.
244251
*
245-
* @private
252+
* @returns {void}
246253
*/
247-
_setupMockInterceptors () {
254+
#setupMockInterceptors () {
248255
for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {
249256
const { request, responses, response } = snapshot
250257
const url = new URL(request.url)
@@ -269,55 +276,68 @@ class SnapshotAgent extends MockAgent {
269276

270277
/**
271278
* Gets the snapshot recorder
279+
* @return {SnapshotRecorder} - The snapshot recorder instance
272280
*/
273281
getRecorder () {
274282
return this[kSnapshotRecorder]
275283
}
276284

277285
/**
278286
* Gets the current mode
287+
* @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode
279288
*/
280289
getMode () {
281290
return this[kSnapshotMode]
282291
}
283292

284293
/**
285294
* Clears all snapshots
295+
* @returns {void}
286296
*/
287297
clearSnapshots () {
288298
this[kSnapshotRecorder].clear()
289299
}
290300

291301
/**
292302
* Resets call counts for all snapshots (useful for test cleanup)
303+
* @returns {void}
293304
*/
294305
resetCallCounts () {
295306
this[kSnapshotRecorder].resetCallCounts()
296307
}
297308

298309
/**
299310
* Deletes a specific snapshot by request options
311+
* @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot
312+
* @return {Promise<boolean>} - Returns true if the snapshot was deleted, false if not found
300313
*/
301314
deleteSnapshot (requestOpts) {
302315
return this[kSnapshotRecorder].deleteSnapshot(requestOpts)
303316
}
304317

305318
/**
306319
* Gets information about a specific snapshot
320+
* @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found
307321
*/
308322
getSnapshotInfo (requestOpts) {
309323
return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)
310324
}
311325

312326
/**
313327
* Replaces all snapshots with new data (full replacement)
328+
* @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record<string, import('./snapshot-recorder').SnapshotEntry>} snapshotData - New snapshot data to replace existing snapshots
329+
* @returns {void}
314330
*/
315331
replaceSnapshots (snapshotData) {
316332
this[kSnapshotRecorder].replaceSnapshots(snapshotData)
317333
}
318334

335+
/**
336+
* Closes the agent, saving snapshots and cleaning up resources.
337+
*
338+
* @returns {Promise<void>}
339+
*/
319340
async close () {
320-
// Close recorder (saves snapshots and cleans up timers)
321341
await this[kSnapshotRecorder].close()
322342
await this[kRealAgent]?.close()
323343
await super.close()

0 commit comments

Comments
 (0)