undici 7.11.0 → 7.13.0
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/README.md +15 -11
- package/docs/docs/api/DiagnosticsChannel.md +7 -4
- package/docs/docs/api/Dispatcher.md +2 -2
- package/docs/docs/api/ProxyAgent.md +1 -1
- package/docs/docs/api/SnapshotAgent.md +616 -0
- package/docs/docs/api/WebSocket.md +27 -0
- package/index.js +5 -1
- package/lib/api/readable.js +49 -29
- package/lib/core/request.js +6 -1
- package/lib/core/tree.js +1 -1
- package/lib/core/util.js +0 -1
- package/lib/dispatcher/client-h1.js +8 -17
- package/lib/dispatcher/proxy-agent.js +67 -71
- package/lib/handler/cache-handler.js +4 -1
- package/lib/handler/redirect-handler.js +12 -2
- package/lib/interceptor/cache.js +2 -2
- package/lib/interceptor/dump.js +2 -1
- package/lib/interceptor/redirect.js +1 -1
- package/lib/mock/mock-agent.js +10 -4
- package/lib/mock/snapshot-agent.js +333 -0
- package/lib/mock/snapshot-recorder.js +517 -0
- package/lib/util/cache.js +1 -1
- package/lib/util/promise.js +28 -0
- package/lib/web/cache/cache.js +10 -8
- package/lib/web/fetch/body.js +35 -24
- package/lib/web/fetch/formdata-parser.js +0 -3
- package/lib/web/fetch/formdata.js +0 -4
- package/lib/web/fetch/index.js +221 -225
- package/lib/web/fetch/request.js +15 -7
- package/lib/web/fetch/response.js +5 -3
- package/lib/web/fetch/util.js +21 -23
- package/lib/web/webidl/index.js +1 -1
- package/lib/web/websocket/connection.js +0 -9
- package/lib/web/websocket/receiver.js +2 -12
- package/lib/web/websocket/stream/websocketstream.js +7 -4
- package/lib/web/websocket/websocket.js +57 -1
- package/package.json +2 -2
- package/types/agent.d.ts +0 -4
- package/types/client.d.ts +0 -2
- package/types/dispatcher.d.ts +0 -6
- package/types/h2c-client.d.ts +0 -2
- package/types/index.d.ts +3 -1
- package/types/mock-interceptor.d.ts +0 -1
- package/types/snapshot-agent.d.ts +107 -0
- package/types/webidl.d.ts +10 -0
- package/types/websocket.d.ts +2 -0
- package/lib/web/fetch/dispatcher-weakref.js +0 -5
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { writeFile, readFile, mkdir } = require('node:fs/promises')
|
|
4
|
+
const { dirname, resolve } = require('node:path')
|
|
5
|
+
const { InvalidArgumentError, UndiciError } = require('../core/errors')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats a request for consistent snapshot storage
|
|
9
|
+
* Caches normalized headers to avoid repeated processing
|
|
10
|
+
*/
|
|
11
|
+
function formatRequestKey (opts, cachedSets, matchOptions = {}) {
|
|
12
|
+
const url = new URL(opts.path, opts.origin)
|
|
13
|
+
|
|
14
|
+
// Cache normalized headers if not already done
|
|
15
|
+
const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
|
|
16
|
+
if (!opts._normalizedHeaders) {
|
|
17
|
+
opts._normalizedHeaders = normalized
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
method: opts.method || 'GET',
|
|
22
|
+
url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
|
|
23
|
+
headers: filterHeadersForMatching(normalized, cachedSets, matchOptions),
|
|
24
|
+
body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : undefined
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filters headers based on matching configuration
|
|
30
|
+
*/
|
|
31
|
+
function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) {
|
|
32
|
+
if (!headers || typeof headers !== 'object') return {}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
matchHeaders = null,
|
|
36
|
+
caseSensitive = false
|
|
37
|
+
} = matchOptions
|
|
38
|
+
|
|
39
|
+
const filtered = {}
|
|
40
|
+
const { ignoreSet, excludeSet, matchSet } = cachedSets
|
|
41
|
+
|
|
42
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
43
|
+
const headerKey = caseSensitive ? key : key.toLowerCase()
|
|
44
|
+
|
|
45
|
+
// Skip if in exclude list (for security)
|
|
46
|
+
if (excludeSet.has(headerKey)) continue
|
|
47
|
+
|
|
48
|
+
// Skip if in ignore list (for matching)
|
|
49
|
+
if (ignoreSet.has(headerKey)) continue
|
|
50
|
+
|
|
51
|
+
// If matchHeaders is specified, only include those headers
|
|
52
|
+
if (matchHeaders && Array.isArray(matchHeaders)) {
|
|
53
|
+
if (!matchSet.has(headerKey)) continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
filtered[headerKey] = value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return filtered
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Filters headers for storage (only excludes sensitive headers)
|
|
64
|
+
*/
|
|
65
|
+
function filterHeadersForStorage (headers, matchOptions = {}) {
|
|
66
|
+
if (!headers || typeof headers !== 'object') return {}
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
excludeHeaders = [],
|
|
70
|
+
caseSensitive = false
|
|
71
|
+
} = matchOptions
|
|
72
|
+
|
|
73
|
+
const filtered = {}
|
|
74
|
+
const excludeSet = new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
|
|
75
|
+
|
|
76
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
77
|
+
const headerKey = caseSensitive ? key : key.toLowerCase()
|
|
78
|
+
|
|
79
|
+
// Skip if in exclude list (for security)
|
|
80
|
+
if (excludeSet.has(headerKey)) continue
|
|
81
|
+
|
|
82
|
+
filtered[headerKey] = value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return filtered
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates cached header sets for performance
|
|
90
|
+
*/
|
|
91
|
+
function createHeaderSetsCache (matchOptions = {}) {
|
|
92
|
+
const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = null, caseSensitive = false } = matchOptions
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ignoreSet: new Set(ignoreHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
|
|
96
|
+
excludeSet: new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
|
|
97
|
+
matchSet: matchHeaders && Array.isArray(matchHeaders)
|
|
98
|
+
? new Set(matchHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
|
|
99
|
+
: null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalizes headers for consistent comparison
|
|
105
|
+
*/
|
|
106
|
+
function normalizeHeaders (headers) {
|
|
107
|
+
if (!headers) return {}
|
|
108
|
+
|
|
109
|
+
const normalized = {}
|
|
110
|
+
|
|
111
|
+
// Handle array format (undici internal format: [name, value, name, value, ...])
|
|
112
|
+
if (Array.isArray(headers)) {
|
|
113
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
114
|
+
const key = headers[i]
|
|
115
|
+
const value = headers[i + 1]
|
|
116
|
+
if (key && value !== undefined) {
|
|
117
|
+
// Convert Buffers to strings if needed
|
|
118
|
+
const keyStr = Buffer.isBuffer(key) ? key.toString() : String(key)
|
|
119
|
+
const valueStr = Buffer.isBuffer(value) ? value.toString() : String(value)
|
|
120
|
+
normalized[keyStr.toLowerCase()] = valueStr
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return normalized
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle object format
|
|
127
|
+
if (headers && typeof headers === 'object') {
|
|
128
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
129
|
+
if (key && typeof key === 'string') {
|
|
130
|
+
normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return normalized
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a hash key for request matching
|
|
140
|
+
*/
|
|
141
|
+
function createRequestHash (request) {
|
|
142
|
+
const parts = [
|
|
143
|
+
request.method,
|
|
144
|
+
request.url,
|
|
145
|
+
JSON.stringify(request.headers, Object.keys(request.headers).sort()),
|
|
146
|
+
request.body || ''
|
|
147
|
+
]
|
|
148
|
+
return Buffer.from(parts.join('|')).toString('base64url')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Checks if a URL matches any of the exclude patterns
|
|
153
|
+
*/
|
|
154
|
+
function isUrlExcluded (url, excludePatterns = []) {
|
|
155
|
+
if (!excludePatterns.length) return false
|
|
156
|
+
|
|
157
|
+
for (const pattern of excludePatterns) {
|
|
158
|
+
if (typeof pattern === 'string') {
|
|
159
|
+
// Simple string match (case-insensitive)
|
|
160
|
+
if (url.toLowerCase().includes(pattern.toLowerCase())) {
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
} else if (pattern instanceof RegExp) {
|
|
164
|
+
// Regex pattern match
|
|
165
|
+
if (pattern.test(url)) {
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class SnapshotRecorder {
|
|
175
|
+
constructor (options = {}) {
|
|
176
|
+
this.snapshots = new Map()
|
|
177
|
+
this.snapshotPath = options.snapshotPath
|
|
178
|
+
this.mode = options.mode || 'record'
|
|
179
|
+
this.loaded = false
|
|
180
|
+
this.maxSnapshots = options.maxSnapshots || Infinity
|
|
181
|
+
this.autoFlush = options.autoFlush || false
|
|
182
|
+
this.flushInterval = options.flushInterval || 30000 // 30 seconds default
|
|
183
|
+
this._flushTimer = null
|
|
184
|
+
this._flushTimeout = null
|
|
185
|
+
|
|
186
|
+
// Matching configuration
|
|
187
|
+
this.matchOptions = {
|
|
188
|
+
matchHeaders: options.matchHeaders || null, // null means match all headers
|
|
189
|
+
ignoreHeaders: options.ignoreHeaders || [],
|
|
190
|
+
excludeHeaders: options.excludeHeaders || [],
|
|
191
|
+
matchBody: options.matchBody !== false, // default: true
|
|
192
|
+
matchQuery: options.matchQuery !== false, // default: true
|
|
193
|
+
caseSensitive: options.caseSensitive || false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Cache processed header sets to avoid recreating them on every request
|
|
197
|
+
this._headerSetsCache = createHeaderSetsCache(this.matchOptions)
|
|
198
|
+
|
|
199
|
+
// Request filtering callbacks
|
|
200
|
+
this.shouldRecord = options.shouldRecord || null // function(requestOpts) -> boolean
|
|
201
|
+
this.shouldPlayback = options.shouldPlayback || null // function(requestOpts) -> boolean
|
|
202
|
+
|
|
203
|
+
// URL pattern filtering
|
|
204
|
+
this.excludeUrls = options.excludeUrls || [] // Array of regex patterns or strings
|
|
205
|
+
|
|
206
|
+
// Start auto-flush timer if enabled
|
|
207
|
+
if (this.autoFlush && this.snapshotPath) {
|
|
208
|
+
this._startAutoFlush()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Records a request-response interaction
|
|
214
|
+
*/
|
|
215
|
+
async record (requestOpts, response) {
|
|
216
|
+
// Check if recording should be filtered out
|
|
217
|
+
if (this.shouldRecord && typeof this.shouldRecord === 'function') {
|
|
218
|
+
if (!this.shouldRecord(requestOpts)) {
|
|
219
|
+
return // Skip recording
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check URL exclusion patterns
|
|
224
|
+
const url = new URL(requestOpts.path, requestOpts.origin).toString()
|
|
225
|
+
if (isUrlExcluded(url, this.excludeUrls)) {
|
|
226
|
+
return // Skip recording
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
|
230
|
+
const hash = createRequestHash(request)
|
|
231
|
+
|
|
232
|
+
// Extract response data - always store body as base64
|
|
233
|
+
const normalizedHeaders = normalizeHeaders(response.headers)
|
|
234
|
+
const responseData = {
|
|
235
|
+
statusCode: response.statusCode,
|
|
236
|
+
headers: filterHeadersForStorage(normalizedHeaders, this.matchOptions),
|
|
237
|
+
body: Buffer.isBuffer(response.body)
|
|
238
|
+
? response.body.toString('base64')
|
|
239
|
+
: Buffer.from(String(response.body || '')).toString('base64'),
|
|
240
|
+
trailers: response.trailers
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Remove oldest snapshot if we exceed maxSnapshots limit
|
|
244
|
+
if (this.snapshots.size >= this.maxSnapshots && !this.snapshots.has(hash)) {
|
|
245
|
+
const oldestKey = this.snapshots.keys().next().value
|
|
246
|
+
this.snapshots.delete(oldestKey)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Support sequential responses - if snapshot exists, add to responses array
|
|
250
|
+
const existingSnapshot = this.snapshots.get(hash)
|
|
251
|
+
if (existingSnapshot && existingSnapshot.responses) {
|
|
252
|
+
existingSnapshot.responses.push(responseData)
|
|
253
|
+
existingSnapshot.timestamp = new Date().toISOString()
|
|
254
|
+
} else {
|
|
255
|
+
this.snapshots.set(hash, {
|
|
256
|
+
request,
|
|
257
|
+
responses: [responseData], // Always store as array for consistency
|
|
258
|
+
callCount: 0,
|
|
259
|
+
timestamp: new Date().toISOString()
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Auto-flush if enabled
|
|
264
|
+
if (this.autoFlush && this.snapshotPath) {
|
|
265
|
+
this._scheduleFlush()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Finds a matching snapshot for the given request
|
|
271
|
+
* Returns the appropriate response based on call count for sequential responses
|
|
272
|
+
*/
|
|
273
|
+
findSnapshot (requestOpts) {
|
|
274
|
+
// Check if playback should be filtered out
|
|
275
|
+
if (this.shouldPlayback && typeof this.shouldPlayback === 'function') {
|
|
276
|
+
if (!this.shouldPlayback(requestOpts)) {
|
|
277
|
+
return undefined // Skip playback
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check URL exclusion patterns
|
|
282
|
+
const url = new URL(requestOpts.path, requestOpts.origin).toString()
|
|
283
|
+
if (isUrlExcluded(url, this.excludeUrls)) {
|
|
284
|
+
return undefined // Skip playback
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
|
288
|
+
const hash = createRequestHash(request)
|
|
289
|
+
const snapshot = this.snapshots.get(hash)
|
|
290
|
+
|
|
291
|
+
if (!snapshot) return undefined
|
|
292
|
+
|
|
293
|
+
// Handle sequential responses
|
|
294
|
+
if (snapshot.responses && Array.isArray(snapshot.responses)) {
|
|
295
|
+
const currentCallCount = snapshot.callCount || 0
|
|
296
|
+
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
|
|
297
|
+
snapshot.callCount = currentCallCount + 1
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
...snapshot,
|
|
301
|
+
response: snapshot.responses[responseIndex]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Legacy format compatibility - convert single response to array format
|
|
306
|
+
if (snapshot.response && !snapshot.responses) {
|
|
307
|
+
snapshot.responses = [snapshot.response]
|
|
308
|
+
snapshot.callCount = 1
|
|
309
|
+
delete snapshot.response
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
...snapshot,
|
|
313
|
+
response: snapshot.responses[0]
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return snapshot
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Loads snapshots from file
|
|
322
|
+
*/
|
|
323
|
+
async loadSnapshots (filePath) {
|
|
324
|
+
const path = filePath || this.snapshotPath
|
|
325
|
+
if (!path) {
|
|
326
|
+
throw new InvalidArgumentError('Snapshot path is required')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const data = await readFile(resolve(path), 'utf8')
|
|
331
|
+
const parsed = JSON.parse(data)
|
|
332
|
+
|
|
333
|
+
// Convert array format back to Map
|
|
334
|
+
if (Array.isArray(parsed)) {
|
|
335
|
+
this.snapshots.clear()
|
|
336
|
+
for (const { hash, snapshot } of parsed) {
|
|
337
|
+
this.snapshots.set(hash, snapshot)
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Legacy object format
|
|
341
|
+
this.snapshots = new Map(Object.entries(parsed))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.loaded = true
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error.code === 'ENOENT') {
|
|
347
|
+
// File doesn't exist yet - that's ok for recording mode
|
|
348
|
+
this.snapshots.clear()
|
|
349
|
+
this.loaded = true
|
|
350
|
+
} else {
|
|
351
|
+
throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Saves snapshots to file
|
|
358
|
+
*/
|
|
359
|
+
async saveSnapshots (filePath) {
|
|
360
|
+
const path = filePath || this.snapshotPath
|
|
361
|
+
if (!path) {
|
|
362
|
+
throw new InvalidArgumentError('Snapshot path is required')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const resolvedPath = resolve(path)
|
|
366
|
+
|
|
367
|
+
// Ensure directory exists
|
|
368
|
+
await mkdir(dirname(resolvedPath), { recursive: true })
|
|
369
|
+
|
|
370
|
+
// Convert Map to serializable format
|
|
371
|
+
const data = Array.from(this.snapshots.entries()).map(([hash, snapshot]) => ({
|
|
372
|
+
hash,
|
|
373
|
+
snapshot
|
|
374
|
+
}))
|
|
375
|
+
|
|
376
|
+
await writeFile(resolvedPath, JSON.stringify(data, null, 2), 'utf8', { flush: true })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Clears all recorded snapshots
|
|
381
|
+
*/
|
|
382
|
+
clear () {
|
|
383
|
+
this.snapshots.clear()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Gets all recorded snapshots
|
|
388
|
+
*/
|
|
389
|
+
getSnapshots () {
|
|
390
|
+
return Array.from(this.snapshots.values())
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Gets snapshot count
|
|
395
|
+
*/
|
|
396
|
+
size () {
|
|
397
|
+
return this.snapshots.size
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Resets call counts for all snapshots (useful for test cleanup)
|
|
402
|
+
*/
|
|
403
|
+
resetCallCounts () {
|
|
404
|
+
for (const snapshot of this.snapshots.values()) {
|
|
405
|
+
snapshot.callCount = 0
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Deletes a specific snapshot by request options
|
|
411
|
+
*/
|
|
412
|
+
deleteSnapshot (requestOpts) {
|
|
413
|
+
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
|
414
|
+
const hash = createRequestHash(request)
|
|
415
|
+
return this.snapshots.delete(hash)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Gets information about a specific snapshot
|
|
420
|
+
*/
|
|
421
|
+
getSnapshotInfo (requestOpts) {
|
|
422
|
+
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
|
423
|
+
const hash = createRequestHash(request)
|
|
424
|
+
const snapshot = this.snapshots.get(hash)
|
|
425
|
+
|
|
426
|
+
if (!snapshot) return null
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
hash,
|
|
430
|
+
request: snapshot.request,
|
|
431
|
+
responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0),
|
|
432
|
+
callCount: snapshot.callCount || 0,
|
|
433
|
+
timestamp: snapshot.timestamp
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Replaces all snapshots with new data (full replacement)
|
|
439
|
+
*/
|
|
440
|
+
replaceSnapshots (snapshotData) {
|
|
441
|
+
this.snapshots.clear()
|
|
442
|
+
|
|
443
|
+
if (Array.isArray(snapshotData)) {
|
|
444
|
+
for (const { hash, snapshot } of snapshotData) {
|
|
445
|
+
this.snapshots.set(hash, snapshot)
|
|
446
|
+
}
|
|
447
|
+
} else if (snapshotData && typeof snapshotData === 'object') {
|
|
448
|
+
// Legacy object format
|
|
449
|
+
this.snapshots = new Map(Object.entries(snapshotData))
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Starts the auto-flush timer
|
|
455
|
+
*/
|
|
456
|
+
_startAutoFlush () {
|
|
457
|
+
if (!this._flushTimer) {
|
|
458
|
+
this._flushTimer = setInterval(() => {
|
|
459
|
+
this.saveSnapshots().catch(() => {
|
|
460
|
+
// Ignore flush errors - they shouldn't interrupt normal operation
|
|
461
|
+
})
|
|
462
|
+
}, this.flushInterval)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Stops the auto-flush timer
|
|
468
|
+
*/
|
|
469
|
+
_stopAutoFlush () {
|
|
470
|
+
if (this._flushTimer) {
|
|
471
|
+
clearInterval(this._flushTimer)
|
|
472
|
+
this._flushTimer = null
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Schedules a flush (debounced to avoid excessive writes)
|
|
478
|
+
*/
|
|
479
|
+
_scheduleFlush () {
|
|
480
|
+
// Simple debouncing - clear existing timeout and set new one
|
|
481
|
+
if (this._flushTimeout) {
|
|
482
|
+
clearTimeout(this._flushTimeout)
|
|
483
|
+
}
|
|
484
|
+
this._flushTimeout = setTimeout(() => {
|
|
485
|
+
this.saveSnapshots().catch(() => {
|
|
486
|
+
// Ignore flush errors
|
|
487
|
+
})
|
|
488
|
+
this._flushTimeout = null
|
|
489
|
+
}, 1000) // 1 second debounce
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Cleanup method to stop timers
|
|
494
|
+
*/
|
|
495
|
+
destroy () {
|
|
496
|
+
this._stopAutoFlush()
|
|
497
|
+
if (this._flushTimeout) {
|
|
498
|
+
clearTimeout(this._flushTimeout)
|
|
499
|
+
this._flushTimeout = null
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Async close method that saves all recordings and performs cleanup
|
|
505
|
+
*/
|
|
506
|
+
async close () {
|
|
507
|
+
// Save any pending recordings if we have a snapshot path
|
|
508
|
+
if (this.snapshotPath && this.snapshots.size > 0) {
|
|
509
|
+
await this.saveSnapshots()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Perform cleanup
|
|
513
|
+
this.destroy()
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, isUrlExcluded, createHeaderSetsCache }
|
package/lib/util/cache.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @template {*} T
|
|
5
|
+
* @typedef {Object} DeferredPromise
|
|
6
|
+
* @property {Promise<T>} promise
|
|
7
|
+
* @property {(value?: T) => void} resolve
|
|
8
|
+
* @property {(reason?: any) => void} reject
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @template {*} T
|
|
13
|
+
* @returns {DeferredPromise<T>} An object containing a promise and its resolve/reject methods.
|
|
14
|
+
*/
|
|
15
|
+
function createDeferredPromise () {
|
|
16
|
+
let res
|
|
17
|
+
let rej
|
|
18
|
+
const promise = new Promise((resolve, reject) => {
|
|
19
|
+
res = resolve
|
|
20
|
+
rej = reject
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return { promise, resolve: res, reject: rej }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
createDeferredPromise
|
|
28
|
+
}
|
package/lib/web/cache/cache.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
|
|
3
5
|
const { kConstruct } = require('../../core/symbols')
|
|
4
6
|
const { urlEquals, getFieldValues } = require('./util')
|
|
5
7
|
const { kEnumerableProperty, isDisturbed } = require('../../core/util')
|
|
@@ -7,8 +9,8 @@ const { webidl } = require('../webidl')
|
|
|
7
9
|
const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response')
|
|
8
10
|
const { Request, fromInnerRequest, getRequestState } = require('../fetch/request')
|
|
9
11
|
const { fetching } = require('../fetch/index')
|
|
10
|
-
const { urlIsHttpHttpsScheme,
|
|
11
|
-
const
|
|
12
|
+
const { urlIsHttpHttpsScheme, readAllBytes } = require('../fetch/util')
|
|
13
|
+
const { createDeferredPromise } = require('../../util/promise')
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
|
|
@@ -46,7 +48,7 @@ class Cache {
|
|
|
46
48
|
const prefix = 'Cache.match'
|
|
47
49
|
webidl.argumentLengthCheck(arguments, 1, prefix)
|
|
48
50
|
|
|
49
|
-
request = webidl.converters.RequestInfo(request
|
|
51
|
+
request = webidl.converters.RequestInfo(request)
|
|
50
52
|
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
|
|
51
53
|
|
|
52
54
|
const p = this.#internalMatchAll(request, options, 1)
|
|
@@ -62,7 +64,7 @@ class Cache {
|
|
|
62
64
|
webidl.brandCheck(this, Cache)
|
|
63
65
|
|
|
64
66
|
const prefix = 'Cache.matchAll'
|
|
65
|
-
if (request !== undefined) request = webidl.converters.RequestInfo(request
|
|
67
|
+
if (request !== undefined) request = webidl.converters.RequestInfo(request)
|
|
66
68
|
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
|
|
67
69
|
|
|
68
70
|
return this.#internalMatchAll(request, options)
|
|
@@ -74,7 +76,7 @@ class Cache {
|
|
|
74
76
|
const prefix = 'Cache.add'
|
|
75
77
|
webidl.argumentLengthCheck(arguments, 1, prefix)
|
|
76
78
|
|
|
77
|
-
request = webidl.converters.RequestInfo(request
|
|
79
|
+
request = webidl.converters.RequestInfo(request)
|
|
78
80
|
|
|
79
81
|
// 1.
|
|
80
82
|
const requests = [request]
|
|
@@ -262,7 +264,7 @@ class Cache {
|
|
|
262
264
|
const prefix = 'Cache.put'
|
|
263
265
|
webidl.argumentLengthCheck(arguments, 2, prefix)
|
|
264
266
|
|
|
265
|
-
request = webidl.converters.RequestInfo(request
|
|
267
|
+
request = webidl.converters.RequestInfo(request)
|
|
266
268
|
response = webidl.converters.Response(response, prefix, 'response')
|
|
267
269
|
|
|
268
270
|
// 1.
|
|
@@ -393,7 +395,7 @@ class Cache {
|
|
|
393
395
|
const prefix = 'Cache.delete'
|
|
394
396
|
webidl.argumentLengthCheck(arguments, 1, prefix)
|
|
395
397
|
|
|
396
|
-
request = webidl.converters.RequestInfo(request
|
|
398
|
+
request = webidl.converters.RequestInfo(request)
|
|
397
399
|
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
|
|
398
400
|
|
|
399
401
|
/**
|
|
@@ -458,7 +460,7 @@ class Cache {
|
|
|
458
460
|
|
|
459
461
|
const prefix = 'Cache.keys'
|
|
460
462
|
|
|
461
|
-
if (request !== undefined) request = webidl.converters.RequestInfo(request
|
|
463
|
+
if (request !== undefined) request = webidl.converters.RequestInfo(request)
|
|
462
464
|
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
|
|
463
465
|
|
|
464
466
|
// 1.
|