mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
deps: update undici to 7.13.0
PR-URL: https://github.com/nodejs/node/pull/59338 Reviewed-By: Matthew Aitken <maitken033380023@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
parent
4f5d11e6fb
commit
3b715d3544
28 changed files with 1924 additions and 364 deletions
20
deps/undici/src/README.md
vendored
20
deps/undici/src/README.md
vendored
|
@ -440,13 +440,14 @@ This behavior is intentional for server-side environments where CORS restriction
|
|||
* https://fetch.spec.whatwg.org/#garbage-collection
|
||||
|
||||
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
|
||||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
|
||||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources.
|
||||
|
||||
Garbage collection in Node is less aggressive and deterministic
|
||||
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
|
||||
which means that leaving the release of connection resources to the garbage collector can lead
|
||||
to excessive connection usage, reduced performance (due to less connection re-use), and even
|
||||
stalls or deadlocks when running out of connections.
|
||||
Therefore, __it is important to always either consume or cancel the response body anyway__.
|
||||
|
||||
```js
|
||||
// Do
|
||||
|
@ -459,7 +460,15 @@ for await (const chunk of body) {
|
|||
const { headers } = await fetch(url);
|
||||
```
|
||||
|
||||
The same applies for `request` too:
|
||||
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
|
||||
|
||||
```js
|
||||
const headers = await fetch(url, { method: 'HEAD' })
|
||||
.then(res => res.headers)
|
||||
```
|
||||
|
||||
Note that consuming the response body is _mandatory_ for `request`:
|
||||
|
||||
```js
|
||||
// Do
|
||||
const { body, headers } = await request(url);
|
||||
|
@ -469,13 +478,6 @@ await res.body.dump(); // force consumption of body
|
|||
const { headers } = await request(url);
|
||||
```
|
||||
|
||||
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
|
||||
|
||||
```js
|
||||
const headers = await fetch(url, { method: 'HEAD' })
|
||||
.then(res => res.headers)
|
||||
```
|
||||
|
||||
#### Forbidden and Safelisted Header Names
|
||||
|
||||
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
|
||||
|
|
2
deps/undici/src/docs/docs/api/ProxyAgent.md
vendored
2
deps/undici/src/docs/docs/api/ProxyAgent.md
vendored
|
@ -27,7 +27,7 @@ For detailed information on the parsing process and potential validation errors,
|
|||
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
|
||||
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
||||
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
||||
* **proxyTunnel** `boolean` (optional) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request.
|
||||
* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address.
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
616
deps/undici/src/docs/docs/api/SnapshotAgent.md
vendored
Normal file
616
deps/undici/src/docs/docs/api/SnapshotAgent.md
vendored
Normal file
|
@ -0,0 +1,616 @@
|
|||
# SnapshotAgent
|
||||
|
||||
The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Integration Testing**: Record real API interactions and replay them in tests
|
||||
- **Offline Development**: Work with APIs without network connectivity
|
||||
- **Consistent Test Data**: Ensure tests use the same responses across runs
|
||||
- **API Contract Testing**: Capture and validate API behavior over time
|
||||
|
||||
## Constructor
|
||||
|
||||
```javascript
|
||||
new SnapshotAgent([options])
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **options** `Object` (optional)
|
||||
- **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'`
|
||||
- **snapshotPath** `String` - Path to the snapshot file for loading/saving
|
||||
- **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity`
|
||||
- **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false`
|
||||
- **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000`
|
||||
- **matchHeaders** `Array<String>` - Specific headers to include in request matching. Default: all headers
|
||||
- **ignoreHeaders** `Array<String>` - Headers to ignore during request matching
|
||||
- **excludeHeaders** `Array<String>` - Headers to exclude from snapshots (for security)
|
||||
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
|
||||
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
|
||||
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
|
||||
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
|
||||
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
|
||||
- **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback
|
||||
- All other options from `MockAgent` are supported
|
||||
|
||||
### Modes
|
||||
|
||||
#### Record Mode (`'record'`)
|
||||
Makes real HTTP requests and saves the responses to snapshots.
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Makes real requests and records them
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
const users = await response.json()
|
||||
|
||||
// Save recorded snapshots
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
#### Playback Mode (`'playback'`)
|
||||
Replays recorded responses without making real HTTP requests.
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Uses recorded response instead of real request
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
```
|
||||
|
||||
#### Update Mode (`'update'`)
|
||||
Uses existing snapshots when available, but records new ones for missing requests.
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'update',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Uses snapshot if exists, otherwise makes real request and records it
|
||||
const response = await fetch('https://api.example.com/new-endpoint')
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
|
||||
### `agent.saveSnapshots([filePath])`
|
||||
|
||||
Saves all recorded snapshots to a file.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise<void>`
|
||||
|
||||
```javascript
|
||||
await agent.saveSnapshots('./custom-snapshots.json')
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Header Filtering
|
||||
|
||||
Control which headers are used for request matching and what gets stored in snapshots:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Only match these specific headers
|
||||
matchHeaders: ['content-type', 'accept'],
|
||||
|
||||
// Ignore these headers during matching (but still store them)
|
||||
ignoreHeaders: ['user-agent', 'date'],
|
||||
|
||||
// Exclude sensitive headers from snapshots entirely
|
||||
excludeHeaders: ['authorization', 'x-api-key', 'cookie']
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Request/Response Filtering
|
||||
|
||||
Use callback functions to determine what gets recorded or played back:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Only record GET requests to specific endpoints
|
||||
shouldRecord: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
|
||||
},
|
||||
|
||||
// Skip authentication endpoints during playback
|
||||
shouldPlayback: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
return !url.pathname.includes('/auth/')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### URL Pattern Exclusion
|
||||
|
||||
Exclude specific URLs from recording/playback using patterns:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
excludeUrls: [
|
||||
'https://analytics.example.com', // String match
|
||||
/\/api\/v\d+\/health/, // Regex pattern
|
||||
'telemetry' // Substring match
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
Configure automatic memory and disk management:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Keep only 1000 snapshots in memory
|
||||
maxSnapshots: 1000,
|
||||
|
||||
// Automatically save to disk every 30 seconds
|
||||
autoFlush: true,
|
||||
flushInterval: 30000
|
||||
})
|
||||
```
|
||||
|
||||
### Sequential Response Handling
|
||||
|
||||
Handle multiple responses for the same request (similar to nock):
|
||||
|
||||
```javascript
|
||||
// In record mode, multiple identical requests get recorded as separate responses
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' })
|
||||
|
||||
// First call returns response A
|
||||
await fetch('https://api.example.com/random')
|
||||
|
||||
// Second call returns response B
|
||||
await fetch('https://api.example.com/random')
|
||||
|
||||
await agent.saveSnapshots()
|
||||
|
||||
// In playback mode, calls return responses in sequence
|
||||
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' })
|
||||
|
||||
// Returns response A
|
||||
const first = await fetch('https://api.example.com/random')
|
||||
|
||||
// Returns response B
|
||||
const second = await fetch('https://api.example.com/random')
|
||||
|
||||
// Third call repeats the last response (B)
|
||||
const third = await fetch('https://api.example.com/random')
|
||||
```
|
||||
|
||||
## Managing Snapshots
|
||||
|
||||
### Replacing Existing Snapshots
|
||||
|
||||
```javascript
|
||||
// Load existing snapshots
|
||||
await agent.loadSnapshots('./old-snapshots.json')
|
||||
|
||||
// Get snapshot data
|
||||
const recorder = agent.getRecorder()
|
||||
const snapshots = recorder.getSnapshots()
|
||||
|
||||
// Modify or filter snapshots
|
||||
const filteredSnapshots = snapshots.filter(s =>
|
||||
!s.request.url.includes('deprecated')
|
||||
)
|
||||
|
||||
// Replace all snapshots
|
||||
agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({
|
||||
hash: `new-hash-${index}`,
|
||||
snapshot
|
||||
})))
|
||||
|
||||
// Save updated snapshots
|
||||
await agent.saveSnapshots('./updated-snapshots.json')
|
||||
```
|
||||
|
||||
### `agent.loadSnapshots([filePath])`
|
||||
|
||||
Loads snapshots from a file.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise<void>`
|
||||
|
||||
```javascript
|
||||
await agent.loadSnapshots('./existing-snapshots.json')
|
||||
```
|
||||
|
||||
### `agent.getRecorder()`
|
||||
|
||||
Gets the underlying `SnapshotRecorder` instance.
|
||||
|
||||
#### Returns
|
||||
|
||||
`SnapshotRecorder`
|
||||
|
||||
```javascript
|
||||
const recorder = agent.getRecorder()
|
||||
console.log(`Recorded ${recorder.size()} interactions`)
|
||||
```
|
||||
|
||||
### `agent.getMode()`
|
||||
|
||||
Gets the current snapshot mode.
|
||||
|
||||
#### Returns
|
||||
|
||||
`String` - The current mode (`'record'`, `'playback'`, or `'update'`)
|
||||
|
||||
### `agent.clearSnapshots()`
|
||||
|
||||
Clears all recorded snapshots from memory.
|
||||
|
||||
```javascript
|
||||
agent.clearSnapshots()
|
||||
```
|
||||
|
||||
## Working with Different Request Types
|
||||
|
||||
### GET Requests
|
||||
|
||||
```javascript
|
||||
// Record mode
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
|
||||
const post = await response.json()
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
### POST Requests with Body
|
||||
|
||||
```javascript
|
||||
// Record mode
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Test Post', body: 'Content' })
|
||||
})
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
### Using with `undici.request`
|
||||
|
||||
SnapshotAgent works with all undici APIs, not just fetch:
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, request, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
const { statusCode, headers, body } = await request('https://api.example.com/data')
|
||||
const data = await body.json()
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
## Test Integration
|
||||
|
||||
### Basic Test Setup
|
||||
|
||||
```javascript
|
||||
import { test } from 'node:test'
|
||||
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
|
||||
|
||||
test('API integration test', async (t) => {
|
||||
const originalDispatcher = getGlobalDispatcher()
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/api-test.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
t.after(() => setGlobalDispatcher(originalDispatcher))
|
||||
|
||||
// This will use recorded data
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
const users = await response.json()
|
||||
|
||||
assert(Array.isArray(users))
|
||||
assert(users.length > 0)
|
||||
})
|
||||
```
|
||||
|
||||
### Environment-Based Mode Selection
|
||||
|
||||
```javascript
|
||||
const mode = process.env.SNAPSHOT_MODE || 'playback'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode,
|
||||
snapshotPath: './test/snapshots/integration.json'
|
||||
})
|
||||
|
||||
// Run with: SNAPSHOT_MODE=record npm test (to record)
|
||||
// Run with: npm test (to playback)
|
||||
```
|
||||
|
||||
### Test Helper Function
|
||||
|
||||
```javascript
|
||||
function createSnapshotAgent(testName, mode = 'playback') {
|
||||
return new SnapshotAgent({
|
||||
mode,
|
||||
snapshotPath: `./test/snapshots/${testName}.json`
|
||||
})
|
||||
}
|
||||
|
||||
test('user API test', async (t) => {
|
||||
const agent = createSnapshotAgent('user-api')
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Test implementation...
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot File Format
|
||||
|
||||
Snapshots are stored as JSON with the following structure:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"hash": "dGVzdC1oYXNo...",
|
||||
"snapshot": {
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://api.example.com/users",
|
||||
"headers": {
|
||||
"authorization": "Bearer token"
|
||||
},
|
||||
"body": undefined
|
||||
},
|
||||
"response": {
|
||||
"statusCode": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded
|
||||
"trailers": {}
|
||||
},
|
||||
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Sensitive Data in Snapshots
|
||||
|
||||
By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Exclude sensitive headers from snapshots
|
||||
excludeHeaders: [
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'cookie',
|
||||
'set-cookie',
|
||||
'x-auth-token',
|
||||
'x-csrf-token'
|
||||
],
|
||||
|
||||
// Filter out requests with sensitive data
|
||||
shouldRecord: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
|
||||
// Don't record authentication endpoints
|
||||
if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't record if request contains sensitive body data
|
||||
if (requestOpts.body && typeof requestOpts.body === 'string') {
|
||||
const body = requestOpts.body.toLowerCase()
|
||||
if (body.includes('password') || body.includes('secret')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Snapshot File Security
|
||||
|
||||
**Important**: Snapshot files may contain sensitive data. Handle them securely:
|
||||
|
||||
- ✅ Add snapshot files to `.gitignore` if they contain real API data
|
||||
- ✅ Use environment-specific snapshots (dev/staging/prod)
|
||||
- ✅ Regularly review snapshot contents for sensitive information
|
||||
- ✅ Use the `excludeHeaders` option for production snapshots
|
||||
- ❌ Never commit snapshots with real authentication tokens
|
||||
- ❌ Don't share snapshot files containing personal data
|
||||
|
||||
```gitignore
|
||||
# Exclude snapshots with real data
|
||||
/test/snapshots/production-*.json
|
||||
/test/snapshots/*-real-data.json
|
||||
|
||||
# Include sanitized test snapshots
|
||||
!/test/snapshots/mock-*.json
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Snapshots in Playback Mode
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch('https://api.example.com/nonexistent')
|
||||
} catch (error) {
|
||||
if (error.message.includes('No snapshot found')) {
|
||||
// Handle missing snapshot
|
||||
console.log('Snapshot not found for this request')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Network Errors in Record Mode
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
||||
|
||||
try {
|
||||
const response = await fetch('https://nonexistent-api.example.com/data')
|
||||
} catch (error) {
|
||||
// Network errors are not recorded as snapshots
|
||||
console.log('Network error:', error.message)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Organize Snapshots by Test Suite
|
||||
|
||||
```javascript
|
||||
// Use descriptive snapshot file names
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json`
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Version Control Snapshots
|
||||
|
||||
Add snapshot files to version control to ensure consistent test behavior across environments:
|
||||
|
||||
```gitignore
|
||||
# Include snapshots in version control
|
||||
!/test/snapshots/*.json
|
||||
```
|
||||
|
||||
### 3. Clean Up Test Data
|
||||
|
||||
```javascript
|
||||
test('API test', async (t) => {
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/temp-test.json'
|
||||
})
|
||||
|
||||
// Clean up after test
|
||||
t.after(() => {
|
||||
agent.clearSnapshots()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Snapshot Validation
|
||||
|
||||
```javascript
|
||||
test('validate snapshot contents', async (t) => {
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/validation.json'
|
||||
})
|
||||
|
||||
const recorder = agent.getRecorder()
|
||||
const snapshots = recorder.getSnapshots()
|
||||
|
||||
// Validate snapshot structure
|
||||
assert(snapshots.length > 0, 'Should have recorded snapshots')
|
||||
assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS')
|
||||
})
|
||||
```
|
||||
|
||||
## Comparison with Other Tools
|
||||
|
||||
### vs Manual MockAgent Setup
|
||||
|
||||
**Manual MockAgent:**
|
||||
```javascript
|
||||
const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('https://api.example.com')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/users',
|
||||
method: 'GET'
|
||||
}).reply(200, [
|
||||
{ id: 1, name: 'User 1' },
|
||||
{ id: 2, name: 'User 2' }
|
||||
])
|
||||
```
|
||||
|
||||
**SnapshotAgent:**
|
||||
```javascript
|
||||
// Record once
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
||||
// Real API call gets recorded automatically
|
||||
|
||||
// Use in tests
|
||||
const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' })
|
||||
// Automatically replays recorded response
|
||||
```
|
||||
|
||||
### vs nock
|
||||
|
||||
SnapshotAgent provides similar functionality to nock but is specifically designed for undici:
|
||||
|
||||
- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.)
|
||||
- ✅ Supports undici-specific features (RetryAgent, connection pooling)
|
||||
- ✅ Better TypeScript integration
|
||||
- ✅ More efficient for high-performance scenarios
|
||||
|
||||
## See Also
|
||||
|
||||
- [MockAgent](./MockAgent.md) - Manual mocking for more control
|
||||
- [MockCallHistory](./MockCallHistory.md) - Inspecting request history
|
||||
- [Testing Best Practices](../best-practices/writing-tests.md) - General testing guidance
|
2
deps/undici/src/index.js
vendored
2
deps/undici/src/index.js
vendored
|
@ -18,6 +18,7 @@ const MockClient = require('./lib/mock/mock-client')
|
|||
const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
|
||||
const MockAgent = require('./lib/mock/mock-agent')
|
||||
const MockPool = require('./lib/mock/mock-pool')
|
||||
const SnapshotAgent = require('./lib/mock/snapshot-agent')
|
||||
const mockErrors = require('./lib/mock/mock-errors')
|
||||
const RetryHandler = require('./lib/handler/retry-handler')
|
||||
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
||||
|
@ -178,6 +179,7 @@ module.exports.MockCallHistory = MockCallHistory
|
|||
module.exports.MockCallHistoryLog = MockCallHistoryLog
|
||||
module.exports.MockPool = MockPool
|
||||
module.exports.MockAgent = MockAgent
|
||||
module.exports.SnapshotAgent = SnapshotAgent
|
||||
module.exports.mockErrors = mockErrors
|
||||
|
||||
const { EventSource } = require('./lib/web/eventsource/eventsource')
|
||||
|
|
70
deps/undici/src/lib/api/readable.js
vendored
70
deps/undici/src/lib/api/readable.js
vendored
|
@ -1,5 +1,3 @@
|
|||
// Ported from https://github.com/nodejs/undici/pull/907
|
||||
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert')
|
||||
|
@ -50,23 +48,32 @@ class BodyReadable extends Readable {
|
|||
|
||||
this[kAbort] = abort
|
||||
|
||||
/**
|
||||
* @type {Consume | null}
|
||||
*/
|
||||
/** @type {Consume | null} */
|
||||
this[kConsume] = null
|
||||
|
||||
/** @type {number} */
|
||||
this[kBytesRead] = 0
|
||||
/**
|
||||
* @type {ReadableStream|null}
|
||||
*/
|
||||
|
||||
/** @type {ReadableStream|null} */
|
||||
this[kBody] = null
|
||||
|
||||
/** @type {boolean} */
|
||||
this[kUsed] = false
|
||||
|
||||
/** @type {string} */
|
||||
this[kContentType] = contentType
|
||||
|
||||
/** @type {number|null} */
|
||||
this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
|
||||
|
||||
// Is stream being consumed through Readable API?
|
||||
// This is an optimization so that we avoid checking
|
||||
// for 'data' and 'readable' listeners in the hot path
|
||||
// inside push().
|
||||
/**
|
||||
* Is stream being consumed through Readable API?
|
||||
* This is an optimization so that we avoid checking
|
||||
* for 'data' and 'readable' listeners in the hot path
|
||||
* inside push().
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this[kReading] = false
|
||||
}
|
||||
|
||||
|
@ -96,7 +103,7 @@ class BodyReadable extends Readable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {string|symbol} event
|
||||
* @param {(...args: any[]) => void} listener
|
||||
* @returns {this}
|
||||
*/
|
||||
|
@ -109,7 +116,7 @@ class BodyReadable extends Readable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {string|symbol} event
|
||||
* @param {(...args: any[]) => void} listener
|
||||
* @returns {this}
|
||||
*/
|
||||
|
@ -147,12 +154,14 @@ class BodyReadable extends Readable {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
push (chunk) {
|
||||
this[kBytesRead] += chunk ? chunk.length : 0
|
||||
|
||||
if (this[kConsume] && chunk !== null) {
|
||||
if (chunk) {
|
||||
this[kBytesRead] += chunk.length
|
||||
if (this[kConsume]) {
|
||||
consumePush(this[kConsume], chunk)
|
||||
return this[kReading] ? super.push(chunk) : true
|
||||
}
|
||||
}
|
||||
|
||||
return super.push(chunk)
|
||||
}
|
||||
|
||||
|
@ -338,9 +347,23 @@ function isUnusable (bodyReadable) {
|
|||
return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {ConsumeType} T
|
||||
* @typedef {T extends 'text' ? string :
|
||||
* T extends 'json' ? unknown :
|
||||
* T extends 'blob' ? Blob :
|
||||
* T extends 'arrayBuffer' ? ArrayBuffer :
|
||||
* T extends 'bytes' ? Uint8Array :
|
||||
* never
|
||||
* } ConsumeReturnType
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} Consume
|
||||
* @property {string} type
|
||||
* @property {ConsumeType} type
|
||||
* @property {BodyReadable} stream
|
||||
* @property {((value?: any) => void)} resolve
|
||||
* @property {((err: Error) => void)} reject
|
||||
|
@ -349,9 +372,10 @@ function isUnusable (bodyReadable) {
|
|||
*/
|
||||
|
||||
/**
|
||||
* @template {ConsumeType} T
|
||||
* @param {BodyReadable} stream
|
||||
* @param {string} type
|
||||
* @returns {Promise<any>}
|
||||
* @param {T} type
|
||||
* @returns {Promise<ConsumeReturnType<T>>}
|
||||
*/
|
||||
function consume (stream, type) {
|
||||
assert(!stream[kConsume])
|
||||
|
@ -361,9 +385,7 @@ function consume (stream, type) {
|
|||
const rState = stream._readableState
|
||||
if (rState.destroyed && rState.closeEmitted === false) {
|
||||
stream
|
||||
.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
.on('error', reject)
|
||||
.on('close', () => {
|
||||
reject(new TypeError('unusable'))
|
||||
})
|
||||
|
@ -438,7 +460,7 @@ function consumeStart (consume) {
|
|||
/**
|
||||
* @param {Buffer[]} chunks
|
||||
* @param {number} length
|
||||
* @param {BufferEncoding} encoding
|
||||
* @param {BufferEncoding} [encoding='utf8']
|
||||
* @returns {string}
|
||||
*/
|
||||
function chunksDecode (chunks, length, encoding) {
|
||||
|
|
1
deps/undici/src/lib/core/util.js
vendored
1
deps/undici/src/lib/core/util.js
vendored
|
@ -5,7 +5,6 @@ const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
|
|||
const { IncomingMessage } = require('node:http')
|
||||
const stream = require('node:stream')
|
||||
const net = require('node:net')
|
||||
const { Blob } = require('node:buffer')
|
||||
const { stringify } = require('node:querystring')
|
||||
const { EventEmitter: EE } = require('node:events')
|
||||
const timers = require('../util/timers')
|
||||
|
|
140
deps/undici/src/lib/dispatcher/proxy-agent.js
vendored
140
deps/undici/src/lib/dispatcher/proxy-agent.js
vendored
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
|
||||
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
||||
const { URL } = require('node:url')
|
||||
const Agent = require('./agent')
|
||||
const Pool = require('./pool')
|
||||
|
@ -27,61 +27,69 @@ function defaultFactory (origin, opts) {
|
|||
|
||||
const noop = () => {}
|
||||
|
||||
class ProxyClient extends DispatcherBase {
|
||||
#client = null
|
||||
constructor (origin, opts) {
|
||||
if (typeof origin === 'string') {
|
||||
origin = new URL(origin)
|
||||
function defaultAgentFactory (origin, opts) {
|
||||
if (opts.connections === 1) {
|
||||
return new Client(origin, opts)
|
||||
}
|
||||
return new Pool(origin, opts)
|
||||
}
|
||||
|
||||
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
|
||||
throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
|
||||
}
|
||||
class Http1ProxyWrapper extends DispatcherBase {
|
||||
#client
|
||||
|
||||
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
||||
super()
|
||||
if (!proxyUrl) {
|
||||
throw new InvalidArgumentError('Proxy URL is mandatory')
|
||||
}
|
||||
|
||||
this.#client = new Client(origin, opts)
|
||||
this[kProxyHeaders] = headers
|
||||
if (factory) {
|
||||
this.#client = factory(proxyUrl, { connect })
|
||||
} else {
|
||||
this.#client = new Client(proxyUrl, { connect })
|
||||
}
|
||||
}
|
||||
|
||||
[kDispatch] (opts, handler) {
|
||||
const onHeaders = handler.onHeaders
|
||||
handler.onHeaders = function (statusCode, data, resume) {
|
||||
if (statusCode === 407) {
|
||||
if (typeof handler.onError === 'function') {
|
||||
handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
|
||||
}
|
||||
return
|
||||
}
|
||||
if (onHeaders) onHeaders.call(this, statusCode, data, resume)
|
||||
}
|
||||
|
||||
// Rewrite request as an HTTP1 Proxy request, without tunneling.
|
||||
const {
|
||||
origin,
|
||||
path = '/',
|
||||
headers = {}
|
||||
} = opts
|
||||
|
||||
opts.path = origin + path
|
||||
|
||||
if (!('host' in headers) && !('Host' in headers)) {
|
||||
const { host } = new URL(origin)
|
||||
headers.host = host
|
||||
}
|
||||
opts.headers = { ...this[kProxyHeaders], ...headers }
|
||||
|
||||
return this.#client[kDispatch](opts, handler)
|
||||
}
|
||||
|
||||
async [kClose] () {
|
||||
await this.#client.close()
|
||||
return this.#client.close()
|
||||
}
|
||||
|
||||
async [kDestroy] () {
|
||||
await this.#client.destroy()
|
||||
async [kDestroy] (err) {
|
||||
return this.#client.destroy(err)
|
||||
}
|
||||
}
|
||||
|
||||
async [kDispatch] (opts, handler) {
|
||||
const { method, origin } = opts
|
||||
if (method === 'CONNECT') {
|
||||
this.#client[kConnector]({
|
||||
origin,
|
||||
port: opts.port || defaultProtocolPort(opts.protocol),
|
||||
path: opts.host,
|
||||
signal: opts.signal,
|
||||
headers: {
|
||||
...this[kProxyHeaders],
|
||||
host: opts.host
|
||||
},
|
||||
servername: this[kProxyTls]?.servername || opts.servername
|
||||
},
|
||||
(err, socket) => {
|
||||
if (err) {
|
||||
handler.callback(err)
|
||||
} else {
|
||||
handler.callback(null, { socket, statusCode: 200 })
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
if (typeof origin === 'string') {
|
||||
opts.origin = new URL(origin)
|
||||
}
|
||||
|
||||
return this.#client.dispatch(opts, handler)
|
||||
}
|
||||
}
|
||||
class ProxyAgent extends DispatcherBase {
|
||||
constructor (opts) {
|
||||
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
|
||||
|
@ -104,6 +112,7 @@ class ProxyAgent extends DispatcherBase {
|
|||
this[kRequestTls] = opts.requestTls
|
||||
this[kProxyTls] = opts.proxyTls
|
||||
this[kProxyHeaders] = opts.headers || {}
|
||||
this[kTunnelProxy] = proxyTunnel
|
||||
|
||||
if (opts.auth && opts.token) {
|
||||
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
|
||||
|
@ -116,21 +125,25 @@ class ProxyAgent extends DispatcherBase {
|
|||
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
|
||||
}
|
||||
|
||||
const factory = (!proxyTunnel && protocol === 'http:')
|
||||
? (origin, options) => {
|
||||
if (origin.protocol === 'http:') {
|
||||
return new ProxyClient(origin, options)
|
||||
}
|
||||
return new Client(origin, options)
|
||||
}
|
||||
: undefined
|
||||
|
||||
const connect = buildConnector({ ...opts.proxyTls })
|
||||
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
|
||||
this[kClient] = clientFactory(url, { connect, factory })
|
||||
this[kTunnelProxy] = proxyTunnel
|
||||
|
||||
const agentFactory = opts.factory || defaultAgentFactory
|
||||
const factory = (origin, options) => {
|
||||
const { protocol } = new URL(origin)
|
||||
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
|
||||
return new Http1ProxyWrapper(this[kProxy].uri, {
|
||||
headers: this[kProxyHeaders],
|
||||
connect,
|
||||
factory: agentFactory
|
||||
})
|
||||
}
|
||||
return agentFactory(origin, options)
|
||||
}
|
||||
this[kClient] = clientFactory(url, { connect })
|
||||
this[kAgent] = new Agent({
|
||||
...opts,
|
||||
factory,
|
||||
connect: async (opts, callback) => {
|
||||
let requestedPath = opts.host
|
||||
if (!opts.port) {
|
||||
|
@ -185,10 +198,6 @@ class ProxyAgent extends DispatcherBase {
|
|||
headers.host = host
|
||||
}
|
||||
|
||||
if (!this.#shouldConnect(new URL(opts.origin))) {
|
||||
opts.path = opts.origin + opts.path
|
||||
}
|
||||
|
||||
return this[kAgent].dispatch(
|
||||
{
|
||||
...opts,
|
||||
|
@ -221,19 +230,6 @@ class ProxyAgent extends DispatcherBase {
|
|||
await this[kAgent].destroy()
|
||||
await this[kClient].destroy()
|
||||
}
|
||||
|
||||
#shouldConnect (uri) {
|
||||
if (typeof uri === 'string') {
|
||||
uri = new URL(uri)
|
||||
}
|
||||
if (this[kTunnelProxy]) {
|
||||
return true
|
||||
}
|
||||
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
10
deps/undici/src/lib/handler/redirect-handler.js
vendored
10
deps/undici/src/lib/handler/redirect-handler.js
vendored
|
@ -133,6 +133,16 @@ class RedirectHandler {
|
|||
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
||||
const path = search ? `${pathname}${search}` : pathname
|
||||
|
||||
// Check for redirect loops by seeing if we've already visited this URL in our history
|
||||
// This catches the case where Client/Pool try to handle cross-origin redirects but fail
|
||||
// and keep redirecting to the same URL in an infinite loop
|
||||
const redirectUrlString = `${origin}${path}`
|
||||
for (const historyUrl of this.history) {
|
||||
if (historyUrl.toString() === redirectUrlString) {
|
||||
throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove headers referring to the original URL.
|
||||
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
|
||||
// https://tools.ietf.org/html/rfc7231#section-6.4
|
||||
|
|
3
deps/undici/src/lib/interceptor/dump.js
vendored
3
deps/undici/src/lib/interceptor/dump.js
vendored
|
@ -57,7 +57,8 @@ class DumpHandler extends DecoratorHandler {
|
|||
return
|
||||
}
|
||||
|
||||
err = this.#controller.reason ?? err
|
||||
// On network errors before connect, controller will be null
|
||||
err = this.#controller?.reason ?? err
|
||||
|
||||
super.onResponseError(controller, err)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
> undici@7.12.0 build:wasm
|
||||
> undici@7.13.0 build:wasm
|
||||
> node build/wasm.js --docker
|
||||
|
||||
> docker run --rm --platform=linux/x86_64 --user 1001:118 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js
|
||||
|
|
14
deps/undici/src/lib/mock/mock-agent.js
vendored
14
deps/undici/src/lib/mock/mock-agent.js
vendored
|
@ -17,7 +17,8 @@ const {
|
|||
kMockAgentAddCallHistoryLog,
|
||||
kMockAgentMockCallHistoryInstance,
|
||||
kMockAgentAcceptsNonStandardSearchParameters,
|
||||
kMockCallHistoryAddLog
|
||||
kMockCallHistoryAddLog,
|
||||
kIgnoreTrailingSlash
|
||||
} = require('./mock-symbols')
|
||||
const MockClient = require('./mock-client')
|
||||
const MockPool = require('./mock-pool')
|
||||
|
@ -37,6 +38,7 @@ class MockAgent extends Dispatcher {
|
|||
this[kIsMockActive] = true
|
||||
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
|
||||
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false
|
||||
this[kIgnoreTrailingSlash] = mockOptions?.ignoreTrailingSlash ?? false
|
||||
|
||||
// Instantiate Agent and encapsulate
|
||||
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
|
||||
|
@ -54,11 +56,15 @@ class MockAgent extends Dispatcher {
|
|||
}
|
||||
|
||||
get (origin) {
|
||||
let dispatcher = this[kMockAgentGet](origin)
|
||||
const originKey = this[kIgnoreTrailingSlash]
|
||||
? origin.replace(/\/$/, '')
|
||||
: origin
|
||||
|
||||
let dispatcher = this[kMockAgentGet](originKey)
|
||||
|
||||
if (!dispatcher) {
|
||||
dispatcher = this[kFactory](origin)
|
||||
this[kMockAgentSet](origin, dispatcher)
|
||||
dispatcher = this[kFactory](originKey)
|
||||
this[kMockAgentSet](originKey, dispatcher)
|
||||
}
|
||||
return dispatcher
|
||||
}
|
||||
|
|
333
deps/undici/src/lib/mock/snapshot-agent.js
vendored
Normal file
333
deps/undici/src/lib/mock/snapshot-agent.js
vendored
Normal file
|
@ -0,0 +1,333 @@
|
|||
'use strict'
|
||||
|
||||
const Agent = require('../dispatcher/agent')
|
||||
const MockAgent = require('./mock-agent')
|
||||
const { SnapshotRecorder } = require('./snapshot-recorder')
|
||||
const WrapHandler = require('../handler/wrap-handler')
|
||||
const { InvalidArgumentError, UndiciError } = require('../core/errors')
|
||||
|
||||
const kSnapshotRecorder = Symbol('kSnapshotRecorder')
|
||||
const kSnapshotMode = Symbol('kSnapshotMode')
|
||||
const kSnapshotPath = Symbol('kSnapshotPath')
|
||||
const kSnapshotLoaded = Symbol('kSnapshotLoaded')
|
||||
const kRealAgent = Symbol('kRealAgent')
|
||||
|
||||
// Static flag to ensure warning is only emitted once
|
||||
let warningEmitted = false
|
||||
|
||||
class SnapshotAgent extends MockAgent {
|
||||
constructor (opts = {}) {
|
||||
// Emit experimental warning only once
|
||||
if (!warningEmitted) {
|
||||
process.emitWarning(
|
||||
'SnapshotAgent is experimental and subject to change',
|
||||
'ExperimentalWarning'
|
||||
)
|
||||
warningEmitted = true
|
||||
}
|
||||
|
||||
const mockOptions = { ...opts }
|
||||
delete mockOptions.mode
|
||||
delete mockOptions.snapshotPath
|
||||
|
||||
super(mockOptions)
|
||||
|
||||
// Validate mode option
|
||||
const validModes = ['record', 'playback', 'update']
|
||||
const mode = opts.mode || 'record'
|
||||
if (!validModes.includes(mode)) {
|
||||
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validModes.join(', ')}`)
|
||||
}
|
||||
|
||||
// Validate snapshotPath is provided when required
|
||||
if ((mode === 'playback' || mode === 'update') && !opts.snapshotPath) {
|
||||
throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)
|
||||
}
|
||||
|
||||
this[kSnapshotMode] = mode
|
||||
this[kSnapshotPath] = opts.snapshotPath
|
||||
this[kSnapshotRecorder] = new SnapshotRecorder({
|
||||
snapshotPath: this[kSnapshotPath],
|
||||
mode: this[kSnapshotMode],
|
||||
maxSnapshots: opts.maxSnapshots,
|
||||
autoFlush: opts.autoFlush,
|
||||
flushInterval: opts.flushInterval,
|
||||
matchHeaders: opts.matchHeaders,
|
||||
ignoreHeaders: opts.ignoreHeaders,
|
||||
excludeHeaders: opts.excludeHeaders,
|
||||
matchBody: opts.matchBody,
|
||||
matchQuery: opts.matchQuery,
|
||||
caseSensitive: opts.caseSensitive,
|
||||
shouldRecord: opts.shouldRecord,
|
||||
shouldPlayback: opts.shouldPlayback,
|
||||
excludeUrls: opts.excludeUrls
|
||||
})
|
||||
this[kSnapshotLoaded] = false
|
||||
|
||||
// For recording/update mode, we need a real agent to make actual requests
|
||||
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update') {
|
||||
this[kRealAgent] = new Agent(opts)
|
||||
}
|
||||
|
||||
// Auto-load snapshots in playback/update mode
|
||||
if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) {
|
||||
this.loadSnapshots().catch(() => {
|
||||
// Ignore load errors - file might not exist yet
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dispatch (opts, handler) {
|
||||
handler = WrapHandler.wrap(handler)
|
||||
const mode = this[kSnapshotMode]
|
||||
|
||||
if (mode === 'playback' || mode === 'update') {
|
||||
// Ensure snapshots are loaded
|
||||
if (!this[kSnapshotLoaded]) {
|
||||
// Need to load asynchronously, delegate to async version
|
||||
return this._asyncDispatch(opts, handler)
|
||||
}
|
||||
|
||||
// Try to find existing snapshot (synchronous)
|
||||
const snapshot = this[kSnapshotRecorder].findSnapshot(opts)
|
||||
|
||||
if (snapshot) {
|
||||
// Use recorded response (synchronous)
|
||||
return this._replaySnapshot(snapshot, handler)
|
||||
} else if (mode === 'update') {
|
||||
// Make real request and record it (async required)
|
||||
return this._recordAndReplay(opts, handler)
|
||||
} else {
|
||||
// Playback mode but no snapshot found
|
||||
const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)
|
||||
if (handler.onError) {
|
||||
handler.onError(error)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} else if (mode === 'record') {
|
||||
// Record mode - make real request and save response (async required)
|
||||
return this._recordAndReplay(opts, handler)
|
||||
} else {
|
||||
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be 'record', 'playback', or 'update'`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of dispatch for when we need to load snapshots first
|
||||
*/
|
||||
async _asyncDispatch (opts, handler) {
|
||||
await this.loadSnapshots()
|
||||
return this.dispatch(opts, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a real request and replays the response
|
||||
*/
|
||||
_recordAndReplay (opts, handler) {
|
||||
const responseData = {
|
||||
statusCode: null,
|
||||
headers: {},
|
||||
trailers: {},
|
||||
body: []
|
||||
}
|
||||
|
||||
const self = this // Capture 'this' context for use within nested handler callbacks
|
||||
|
||||
const recordingHandler = {
|
||||
onRequestStart (controller, context) {
|
||||
return handler.onRequestStart(controller, { ...context, history: this.history })
|
||||
},
|
||||
|
||||
onRequestUpgrade (controller, statusCode, headers, socket) {
|
||||
return handler.onRequestUpgrade(controller, statusCode, headers, socket)
|
||||
},
|
||||
|
||||
onResponseStart (controller, statusCode, headers, statusMessage) {
|
||||
responseData.statusCode = statusCode
|
||||
responseData.headers = headers
|
||||
return handler.onResponseStart(controller, statusCode, headers, statusMessage)
|
||||
},
|
||||
|
||||
onResponseData (controller, chunk) {
|
||||
responseData.body.push(chunk)
|
||||
return handler.onResponseData(controller, chunk)
|
||||
},
|
||||
|
||||
onResponseEnd (controller, trailers) {
|
||||
responseData.trailers = trailers
|
||||
|
||||
// Record the interaction using captured 'self' context (fire and forget)
|
||||
const responseBody = Buffer.concat(responseData.body)
|
||||
self[kSnapshotRecorder].record(opts, {
|
||||
statusCode: responseData.statusCode,
|
||||
headers: responseData.headers,
|
||||
body: responseBody,
|
||||
trailers: responseData.trailers
|
||||
}).then(() => {
|
||||
handler.onResponseEnd(controller, trailers)
|
||||
}).catch((error) => {
|
||||
handler.onResponseError(controller, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Use composed agent if available (includes interceptors), otherwise use real agent
|
||||
const agent = this[kRealAgent]
|
||||
return agent.dispatch(opts, recordingHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays a recorded response
|
||||
*/
|
||||
_replaySnapshot (snapshot, handler) {
|
||||
return new Promise((resolve) => {
|
||||
// Simulate the response
|
||||
setImmediate(() => {
|
||||
try {
|
||||
const { response } = snapshot
|
||||
|
||||
const controller = {
|
||||
pause () {},
|
||||
resume () {},
|
||||
abort (reason) {
|
||||
this.aborted = true
|
||||
this.reason = reason
|
||||
},
|
||||
|
||||
aborted: false,
|
||||
paused: false
|
||||
}
|
||||
|
||||
handler.onRequestStart(controller)
|
||||
|
||||
handler.onResponseStart(controller, response.statusCode, response.headers)
|
||||
|
||||
// Body is always stored as base64 string
|
||||
const body = Buffer.from(response.body, 'base64')
|
||||
handler.onResponseData(controller, body)
|
||||
|
||||
handler.onResponseEnd(controller, response.trailers)
|
||||
resolve()
|
||||
} catch (error) {
|
||||
handler.onError?.(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads snapshots from file
|
||||
*/
|
||||
async loadSnapshots (filePath) {
|
||||
await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])
|
||||
this[kSnapshotLoaded] = true
|
||||
|
||||
// In playback mode, set up MockAgent interceptors for all snapshots
|
||||
if (this[kSnapshotMode] === 'playback') {
|
||||
this._setupMockInterceptors()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves snapshots to file
|
||||
*/
|
||||
async saveSnapshots (filePath) {
|
||||
return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up MockAgent interceptors based on recorded snapshots.
|
||||
*
|
||||
* This method creates MockAgent interceptors for each recorded snapshot,
|
||||
* allowing the SnapshotAgent to fall back to MockAgent's standard intercept
|
||||
* mechanism in playback mode. Each interceptor is configured to persist
|
||||
* (remain active for multiple requests) and responds with the recorded
|
||||
* response data.
|
||||
*
|
||||
* Called automatically when loading snapshots in playback mode.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setupMockInterceptors () {
|
||||
for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {
|
||||
const { request, responses, response } = snapshot
|
||||
const url = new URL(request.url)
|
||||
|
||||
const mockPool = this.get(url.origin)
|
||||
|
||||
// Handle both new format (responses array) and legacy format (response object)
|
||||
const responseData = responses ? responses[0] : response
|
||||
if (!responseData) continue
|
||||
|
||||
mockPool.intercept({
|
||||
path: url.pathname + url.search,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body
|
||||
}).reply(responseData.statusCode, responseData.body, {
|
||||
headers: responseData.headers,
|
||||
trailers: responseData.trailers
|
||||
}).persist()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snapshot recorder
|
||||
*/
|
||||
getRecorder () {
|
||||
return this[kSnapshotRecorder]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current mode
|
||||
*/
|
||||
getMode () {
|
||||
return this[kSnapshotMode]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all snapshots
|
||||
*/
|
||||
clearSnapshots () {
|
||||
this[kSnapshotRecorder].clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets call counts for all snapshots (useful for test cleanup)
|
||||
*/
|
||||
resetCallCounts () {
|
||||
this[kSnapshotRecorder].resetCallCounts()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific snapshot by request options
|
||||
*/
|
||||
deleteSnapshot (requestOpts) {
|
||||
return this[kSnapshotRecorder].deleteSnapshot(requestOpts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about a specific snapshot
|
||||
*/
|
||||
getSnapshotInfo (requestOpts) {
|
||||
return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all snapshots with new data (full replacement)
|
||||
*/
|
||||
replaceSnapshots (snapshotData) {
|
||||
this[kSnapshotRecorder].replaceSnapshots(snapshotData)
|
||||
}
|
||||
|
||||
async close () {
|
||||
// Close recorder (saves snapshots and cleans up timers)
|
||||
await this[kSnapshotRecorder].close()
|
||||
await this[kRealAgent]?.close()
|
||||
await super.close()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SnapshotAgent
|
517
deps/undici/src/lib/mock/snapshot-recorder.js
vendored
Normal file
517
deps/undici/src/lib/mock/snapshot-recorder.js
vendored
Normal file
|
@ -0,0 +1,517 @@
|
|||
'use strict'
|
||||
|
||||
const { writeFile, readFile, mkdir } = require('node:fs/promises')
|
||||
const { dirname, resolve } = require('node:path')
|
||||
const { InvalidArgumentError, UndiciError } = require('../core/errors')
|
||||
|
||||
/**
|
||||
* Formats a request for consistent snapshot storage
|
||||
* Caches normalized headers to avoid repeated processing
|
||||
*/
|
||||
function formatRequestKey (opts, cachedSets, matchOptions = {}) {
|
||||
const url = new URL(opts.path, opts.origin)
|
||||
|
||||
// Cache normalized headers if not already done
|
||||
const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
|
||||
if (!opts._normalizedHeaders) {
|
||||
opts._normalizedHeaders = normalized
|
||||
}
|
||||
|
||||
return {
|
||||
method: opts.method || 'GET',
|
||||
url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
|
||||
headers: filterHeadersForMatching(normalized, cachedSets, matchOptions),
|
||||
body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters headers based on matching configuration
|
||||
*/
|
||||
function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) {
|
||||
if (!headers || typeof headers !== 'object') return {}
|
||||
|
||||
const {
|
||||
matchHeaders = null,
|
||||
caseSensitive = false
|
||||
} = matchOptions
|
||||
|
||||
const filtered = {}
|
||||
const { ignoreSet, excludeSet, matchSet } = cachedSets
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
const headerKey = caseSensitive ? key : key.toLowerCase()
|
||||
|
||||
// Skip if in exclude list (for security)
|
||||
if (excludeSet.has(headerKey)) continue
|
||||
|
||||
// Skip if in ignore list (for matching)
|
||||
if (ignoreSet.has(headerKey)) continue
|
||||
|
||||
// If matchHeaders is specified, only include those headers
|
||||
if (matchHeaders && Array.isArray(matchHeaders)) {
|
||||
if (!matchSet.has(headerKey)) continue
|
||||
}
|
||||
|
||||
filtered[headerKey] = value
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters headers for storage (only excludes sensitive headers)
|
||||
*/
|
||||
function filterHeadersForStorage (headers, matchOptions = {}) {
|
||||
if (!headers || typeof headers !== 'object') return {}
|
||||
|
||||
const {
|
||||
excludeHeaders = [],
|
||||
caseSensitive = false
|
||||
} = matchOptions
|
||||
|
||||
const filtered = {}
|
||||
const excludeSet = new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
const headerKey = caseSensitive ? key : key.toLowerCase()
|
||||
|
||||
// Skip if in exclude list (for security)
|
||||
if (excludeSet.has(headerKey)) continue
|
||||
|
||||
filtered[headerKey] = value
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates cached header sets for performance
|
||||
*/
|
||||
function createHeaderSetsCache (matchOptions = {}) {
|
||||
const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = null, caseSensitive = false } = matchOptions
|
||||
|
||||
return {
|
||||
ignoreSet: new Set(ignoreHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
|
||||
excludeSet: new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
|
||||
matchSet: matchHeaders && Array.isArray(matchHeaders)
|
||||
? new Set(matchHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes headers for consistent comparison
|
||||
*/
|
||||
function normalizeHeaders (headers) {
|
||||
if (!headers) return {}
|
||||
|
||||
const normalized = {}
|
||||
|
||||
// Handle array format (undici internal format: [name, value, name, value, ...])
|
||||
if (Array.isArray(headers)) {
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
const key = headers[i]
|
||||
const value = headers[i + 1]
|
||||
if (key && value !== undefined) {
|
||||
// Convert Buffers to strings if needed
|
||||
const keyStr = Buffer.isBuffer(key) ? key.toString() : String(key)
|
||||
const valueStr = Buffer.isBuffer(value) ? value.toString() : String(value)
|
||||
normalized[keyStr.toLowerCase()] = valueStr
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// Handle object format
|
||||
if (headers && typeof headers === 'object') {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key && typeof key === 'string') {
|
||||
normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hash key for request matching
|
||||
*/
|
||||
function createRequestHash (request) {
|
||||
const parts = [
|
||||
request.method,
|
||||
request.url,
|
||||
JSON.stringify(request.headers, Object.keys(request.headers).sort()),
|
||||
request.body || ''
|
||||
]
|
||||
return Buffer.from(parts.join('|')).toString('base64url')
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL matches any of the exclude patterns
|
||||
*/
|
||||
function isUrlExcluded (url, excludePatterns = []) {
|
||||
if (!excludePatterns.length) return false
|
||||
|
||||
for (const pattern of excludePatterns) {
|
||||
if (typeof pattern === 'string') {
|
||||
// Simple string match (case-insensitive)
|
||||
if (url.toLowerCase().includes(pattern.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
} else if (pattern instanceof RegExp) {
|
||||
// Regex pattern match
|
||||
if (pattern.test(url)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class SnapshotRecorder {
|
||||
constructor (options = {}) {
|
||||
this.snapshots = new Map()
|
||||
this.snapshotPath = options.snapshotPath
|
||||
this.mode = options.mode || 'record'
|
||||
this.loaded = false
|
||||
this.maxSnapshots = options.maxSnapshots || Infinity
|
||||
this.autoFlush = options.autoFlush || false
|
||||
this.flushInterval = options.flushInterval || 30000 // 30 seconds default
|
||||
this._flushTimer = null
|
||||
this._flushTimeout = null
|
||||
|
||||
// Matching configuration
|
||||
this.matchOptions = {
|
||||
matchHeaders: options.matchHeaders || null, // null means match all headers
|
||||
ignoreHeaders: options.ignoreHeaders || [],
|
||||
excludeHeaders: options.excludeHeaders || [],
|
||||
matchBody: options.matchBody !== false, // default: true
|
||||
matchQuery: options.matchQuery !== false, // default: true
|
||||
caseSensitive: options.caseSensitive || false
|
||||
}
|
||||
|
||||
// Cache processed header sets to avoid recreating them on every request
|
||||
this._headerSetsCache = createHeaderSetsCache(this.matchOptions)
|
||||
|
||||
// Request filtering callbacks
|
||||
this.shouldRecord = options.shouldRecord || null // function(requestOpts) -> boolean
|
||||
this.shouldPlayback = options.shouldPlayback || null // function(requestOpts) -> boolean
|
||||
|
||||
// URL pattern filtering
|
||||
this.excludeUrls = options.excludeUrls || [] // Array of regex patterns or strings
|
||||
|
||||
// Start auto-flush timer if enabled
|
||||
if (this.autoFlush && this.snapshotPath) {
|
||||
this._startAutoFlush()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a request-response interaction
|
||||
*/
|
||||
async record (requestOpts, response) {
|
||||
// Check if recording should be filtered out
|
||||
if (this.shouldRecord && typeof this.shouldRecord === 'function') {
|
||||
if (!this.shouldRecord(requestOpts)) {
|
||||
return // Skip recording
|
||||
}
|
||||
}
|
||||
|
||||
// Check URL exclusion patterns
|
||||
const url = new URL(requestOpts.path, requestOpts.origin).toString()
|
||||
if (isUrlExcluded(url, this.excludeUrls)) {
|
||||
return // Skip recording
|
||||
}
|
||||
|
||||
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
||||
const hash = createRequestHash(request)
|
||||
|
||||
// Extract response data - always store body as base64
|
||||
const normalizedHeaders = normalizeHeaders(response.headers)
|
||||
const responseData = {
|
||||
statusCode: response.statusCode,
|
||||
headers: filterHeadersForStorage(normalizedHeaders, this.matchOptions),
|
||||
body: Buffer.isBuffer(response.body)
|
||||
? response.body.toString('base64')
|
||||
: Buffer.from(String(response.body || '')).toString('base64'),
|
||||
trailers: response.trailers
|
||||
}
|
||||
|
||||
// Remove oldest snapshot if we exceed maxSnapshots limit
|
||||
if (this.snapshots.size >= this.maxSnapshots && !this.snapshots.has(hash)) {
|
||||
const oldestKey = this.snapshots.keys().next().value
|
||||
this.snapshots.delete(oldestKey)
|
||||
}
|
||||
|
||||
// Support sequential responses - if snapshot exists, add to responses array
|
||||
const existingSnapshot = this.snapshots.get(hash)
|
||||
if (existingSnapshot && existingSnapshot.responses) {
|
||||
existingSnapshot.responses.push(responseData)
|
||||
existingSnapshot.timestamp = new Date().toISOString()
|
||||
} else {
|
||||
this.snapshots.set(hash, {
|
||||
request,
|
||||
responses: [responseData], // Always store as array for consistency
|
||||
callCount: 0,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-flush if enabled
|
||||
if (this.autoFlush && this.snapshotPath) {
|
||||
this._scheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a matching snapshot for the given request
|
||||
* Returns the appropriate response based on call count for sequential responses
|
||||
*/
|
||||
findSnapshot (requestOpts) {
|
||||
// Check if playback should be filtered out
|
||||
if (this.shouldPlayback && typeof this.shouldPlayback === 'function') {
|
||||
if (!this.shouldPlayback(requestOpts)) {
|
||||
return undefined // Skip playback
|
||||
}
|
||||
}
|
||||
|
||||
// Check URL exclusion patterns
|
||||
const url = new URL(requestOpts.path, requestOpts.origin).toString()
|
||||
if (isUrlExcluded(url, this.excludeUrls)) {
|
||||
return undefined // Skip playback
|
||||
}
|
||||
|
||||
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
||||
const hash = createRequestHash(request)
|
||||
const snapshot = this.snapshots.get(hash)
|
||||
|
||||
if (!snapshot) return undefined
|
||||
|
||||
// Handle sequential responses
|
||||
if (snapshot.responses && Array.isArray(snapshot.responses)) {
|
||||
const currentCallCount = snapshot.callCount || 0
|
||||
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
|
||||
snapshot.callCount = currentCallCount + 1
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
response: snapshot.responses[responseIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy format compatibility - convert single response to array format
|
||||
if (snapshot.response && !snapshot.responses) {
|
||||
snapshot.responses = [snapshot.response]
|
||||
snapshot.callCount = 1
|
||||
delete snapshot.response
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
response: snapshot.responses[0]
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads snapshots from file
|
||||
*/
|
||||
async loadSnapshots (filePath) {
|
||||
const path = filePath || this.snapshotPath
|
||||
if (!path) {
|
||||
throw new InvalidArgumentError('Snapshot path is required')
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await readFile(resolve(path), 'utf8')
|
||||
const parsed = JSON.parse(data)
|
||||
|
||||
// Convert array format back to Map
|
||||
if (Array.isArray(parsed)) {
|
||||
this.snapshots.clear()
|
||||
for (const { hash, snapshot } of parsed) {
|
||||
this.snapshots.set(hash, snapshot)
|
||||
}
|
||||
} else {
|
||||
// Legacy object format
|
||||
this.snapshots = new Map(Object.entries(parsed))
|
||||
}
|
||||
|
||||
this.loaded = true
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// File doesn't exist yet - that's ok for recording mode
|
||||
this.snapshots.clear()
|
||||
this.loaded = true
|
||||
} else {
|
||||
throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves snapshots to file
|
||||
*/
|
||||
async saveSnapshots (filePath) {
|
||||
const path = filePath || this.snapshotPath
|
||||
if (!path) {
|
||||
throw new InvalidArgumentError('Snapshot path is required')
|
||||
}
|
||||
|
||||
const resolvedPath = resolve(path)
|
||||
|
||||
// Ensure directory exists
|
||||
await mkdir(dirname(resolvedPath), { recursive: true })
|
||||
|
||||
// Convert Map to serializable format
|
||||
const data = Array.from(this.snapshots.entries()).map(([hash, snapshot]) => ({
|
||||
hash,
|
||||
snapshot
|
||||
}))
|
||||
|
||||
await writeFile(resolvedPath, JSON.stringify(data, null, 2), 'utf8', { flush: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all recorded snapshots
|
||||
*/
|
||||
clear () {
|
||||
this.snapshots.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all recorded snapshots
|
||||
*/
|
||||
getSnapshots () {
|
||||
return Array.from(this.snapshots.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets snapshot count
|
||||
*/
|
||||
size () {
|
||||
return this.snapshots.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets call counts for all snapshots (useful for test cleanup)
|
||||
*/
|
||||
resetCallCounts () {
|
||||
for (const snapshot of this.snapshots.values()) {
|
||||
snapshot.callCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific snapshot by request options
|
||||
*/
|
||||
deleteSnapshot (requestOpts) {
|
||||
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
||||
const hash = createRequestHash(request)
|
||||
return this.snapshots.delete(hash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about a specific snapshot
|
||||
*/
|
||||
getSnapshotInfo (requestOpts) {
|
||||
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
|
||||
const hash = createRequestHash(request)
|
||||
const snapshot = this.snapshots.get(hash)
|
||||
|
||||
if (!snapshot) return null
|
||||
|
||||
return {
|
||||
hash,
|
||||
request: snapshot.request,
|
||||
responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0),
|
||||
callCount: snapshot.callCount || 0,
|
||||
timestamp: snapshot.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all snapshots with new data (full replacement)
|
||||
*/
|
||||
replaceSnapshots (snapshotData) {
|
||||
this.snapshots.clear()
|
||||
|
||||
if (Array.isArray(snapshotData)) {
|
||||
for (const { hash, snapshot } of snapshotData) {
|
||||
this.snapshots.set(hash, snapshot)
|
||||
}
|
||||
} else if (snapshotData && typeof snapshotData === 'object') {
|
||||
// Legacy object format
|
||||
this.snapshots = new Map(Object.entries(snapshotData))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto-flush timer
|
||||
*/
|
||||
_startAutoFlush () {
|
||||
if (!this._flushTimer) {
|
||||
this._flushTimer = setInterval(() => {
|
||||
this.saveSnapshots().catch(() => {
|
||||
// Ignore flush errors - they shouldn't interrupt normal operation
|
||||
})
|
||||
}, this.flushInterval)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the auto-flush timer
|
||||
*/
|
||||
_stopAutoFlush () {
|
||||
if (this._flushTimer) {
|
||||
clearInterval(this._flushTimer)
|
||||
this._flushTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a flush (debounced to avoid excessive writes)
|
||||
*/
|
||||
_scheduleFlush () {
|
||||
// Simple debouncing - clear existing timeout and set new one
|
||||
if (this._flushTimeout) {
|
||||
clearTimeout(this._flushTimeout)
|
||||
}
|
||||
this._flushTimeout = setTimeout(() => {
|
||||
this.saveSnapshots().catch(() => {
|
||||
// Ignore flush errors
|
||||
})
|
||||
this._flushTimeout = null
|
||||
}, 1000) // 1 second debounce
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method to stop timers
|
||||
*/
|
||||
destroy () {
|
||||
this._stopAutoFlush()
|
||||
if (this._flushTimeout) {
|
||||
clearTimeout(this._flushTimeout)
|
||||
this._flushTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async close method that saves all recordings and performs cleanup
|
||||
*/
|
||||
async close () {
|
||||
// Save any pending recordings if we have a snapshot path
|
||||
if (this.snapshotPath && this.snapshots.size > 0) {
|
||||
await this.saveSnapshots()
|
||||
}
|
||||
|
||||
// Perform cleanup
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, isUrlExcluded, createHeaderSetsCache }
|
1
deps/undici/src/lib/web/fetch/body.js
vendored
1
deps/undici/src/lib/web/fetch/body.js
vendored
|
@ -10,7 +10,6 @@ const {
|
|||
} = require('./util')
|
||||
const { FormData, setFormDataState } = require('./formdata')
|
||||
const { webidl } = require('../webidl')
|
||||
const { Blob } = require('node:buffer')
|
||||
const assert = require('node:assert')
|
||||
const { isErrored, isDisturbed } = require('node:stream')
|
||||
const { isArrayBuffer } = require('node:util/types')
|
||||
|
|
|
@ -6,9 +6,6 @@ const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|||
const { makeEntry } = require('./formdata')
|
||||
const { webidl } = require('../webidl')
|
||||
const assert = require('node:assert')
|
||||
const { File: NodeFile } = require('node:buffer')
|
||||
|
||||
const File = globalThis.File ?? NodeFile
|
||||
|
||||
const formDataNameBuffer = Buffer.from('form-data; name="')
|
||||
const filenameBuffer = Buffer.from('filename')
|
||||
|
|
4
deps/undici/src/lib/web/fetch/formdata.js
vendored
4
deps/undici/src/lib/web/fetch/formdata.js
vendored
|
@ -3,12 +3,8 @@
|
|||
const { iteratorMixin } = require('./util')
|
||||
const { kEnumerableProperty } = require('../../core/util')
|
||||
const { webidl } = require('../webidl')
|
||||
const { File: NativeFile } = require('node:buffer')
|
||||
const nodeUtil = require('node:util')
|
||||
|
||||
/** @type {globalThis['File']} */
|
||||
const File = globalThis.File ?? NativeFile
|
||||
|
||||
// https://xhr.spec.whatwg.org/#formdata
|
||||
class FormData {
|
||||
#state = []
|
||||
|
|
2
deps/undici/src/lib/web/webidl/index.js
vendored
2
deps/undici/src/lib/web/webidl/index.js
vendored
|
@ -509,7 +509,7 @@ webidl.is.USVString = function (value) {
|
|||
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
|
||||
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
|
||||
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
|
||||
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
|
||||
webidl.is.File = webidl.util.MakeTypeAssertion(File)
|
||||
webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
|
||||
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
|
||||
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
|
||||
|
|
249
deps/undici/src/package-lock.json
generated
vendored
249
deps/undici/src/package-lock.json
generated
vendored
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "undici",
|
||||
"version": "7.12.0",
|
||||
"version": "7.13.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "undici",
|
||||
"version": "7.12.0",
|
||||
"version": "7.13.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@fastify/busboy": "3.1.1",
|
||||
|
@ -261,14 +261,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
|
||||
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
|
||||
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.6"
|
||||
"@babel/types": "^7.28.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -564,9 +564,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
|
||||
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -1168,9 +1168,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
||||
"version": "9.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
|
||||
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -1191,9 +1191,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
|
||||
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -1959,9 +1959,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@reporters/github": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.7.2.tgz",
|
||||
"integrity": "sha512-8mvTyKUxxDXkNIWfzv3FsHVwjr8JCwVtwidQws2neV6YgrsJW6OwTOBBhd01RKrDMXPxgpMQuFEfN9hRuUZGuA==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.8.0.tgz",
|
||||
"integrity": "sha512-EJNbv7qvqbICrVbyaPLKWT/mGzdkkdskKuPg1hG0tVKeAEtH6D1gCZwZ84N/26CQ8FBsyfiUyVjwtgYEByGKWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -2106,13 +2106,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
|
||||
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
|
@ -2185,9 +2185,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.120",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz",
|
||||
"integrity": "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==",
|
||||
"version": "18.19.121",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz",
|
||||
"integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -2236,17 +2236,17 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
|
||||
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/type-utils": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/type-utils": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
|
@ -2260,7 +2260,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
|
@ -2276,16 +2276,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
||||
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2301,14 +2301,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
|
||||
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.37.0",
|
||||
"@typescript-eslint/types": "^8.37.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2323,14 +2323,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
|
||||
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
||||
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0"
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
@ -2341,9 +2341,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -2358,15 +2358,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
|
||||
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
|
@ -2383,9 +2383,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
|
||||
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
||||
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -2397,16 +2397,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
|
||||
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.37.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/visitor-keys": "8.37.0",
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
|
@ -2465,16 +2465,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
|
||||
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0"
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
@ -2489,13 +2489,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
|
||||
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
||||
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -3302,9 +3302,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/babel-preset-current-node-syntax": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
|
||||
"integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
||||
"integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -3325,7 +3325,7 @@
|
|||
"@babel/plugin-syntax-top-level-await": "^7.14.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
"@babel/core": "^7.0.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-jest": {
|
||||
|
@ -3359,9 +3359,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/borp": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/borp/-/borp-0.20.0.tgz",
|
||||
"integrity": "sha512-SZhSNosPoX6c9gtXKnakpkUYdAyQMkQDQwPzpPoIBV9ErWhWH2ks6paai27R37O/MNC9jLMW1lQojuZEOAF87g==",
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/borp/-/borp-0.20.1.tgz",
|
||||
"integrity": "sha512-+1juusmbzAetePd0AIGbj+wut27bmzLFa2BhP8J9VGNcV7sx7bs4pDR2DhOU2oDZestWZpUzjfgIKYEI7LDW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -3632,9 +3632,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001727",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
|
||||
"version": "1.0.30001731",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4157,9 +4157,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.187",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
||||
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
|
||||
"version": "1.5.194",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
|
||||
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
@ -4450,9 +4450,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
||||
"version": "9.32.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -4462,8 +4462,8 @@
|
|||
"@eslint/config-helpers": "^0.3.0",
|
||||
"@eslint/core": "^0.15.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.31.0",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@eslint/js": "9.32.0",
|
||||
"@eslint/plugin-kit": "^0.3.4",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
|
@ -4756,9 +4756,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-n": {
|
||||
"version": "17.21.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz",
|
||||
"integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==",
|
||||
"version": "17.21.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz",
|
||||
"integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -4767,8 +4767,8 @@
|
|||
"eslint-plugin-es-x": "^7.8.0",
|
||||
"get-tsconfig": "^4.8.1",
|
||||
"globals": "^15.11.0",
|
||||
"globrex": "^0.1.2",
|
||||
"ignore": "^5.3.2",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.6.3",
|
||||
"ts-declaration-location": "^1.0.6"
|
||||
},
|
||||
|
@ -4782,16 +4782,6 @@
|
|||
"eslint": ">=8.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-n/node_modules/globals": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
|
@ -4805,22 +4795,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-n/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-n/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
@ -5640,6 +5614,13 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globrex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
@ -10066,16 +10047,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.37.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz",
|
||||
"integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==",
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz",
|
||||
"integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.37.0",
|
||||
"@typescript-eslint/parser": "8.37.0",
|
||||
"@typescript-eslint/typescript-estree": "8.37.0",
|
||||
"@typescript-eslint/utils": "8.37.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.38.0",
|
||||
"@typescript-eslint/parser": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
2
deps/undici/src/package.json
vendored
2
deps/undici/src/package.json
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "undici",
|
||||
"version": "7.12.0",
|
||||
"version": "7.13.0",
|
||||
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
||||
"homepage": "https://undici.nodejs.org",
|
||||
"bugs": {
|
||||
|
|
4
deps/undici/src/types/agent.d.ts
vendored
4
deps/undici/src/types/agent.d.ts
vendored
|
@ -22,14 +22,10 @@ declare namespace Agent {
|
|||
export interface Options extends Pool.Options {
|
||||
/** Default: `(origin, opts) => new Pool(origin, opts)`. */
|
||||
factory?(origin: string | URL, opts: Object): Dispatcher;
|
||||
/** Integer. Default: `0` */
|
||||
maxRedirections?: number;
|
||||
|
||||
interceptors?: { Agent?: readonly Dispatcher.DispatchInterceptor[] } & Pool.Options['interceptors']
|
||||
}
|
||||
|
||||
export interface DispatchOptions extends Dispatcher.DispatchOptions {
|
||||
/** Integer. */
|
||||
maxRedirections?: number;
|
||||
}
|
||||
}
|
||||
|
|
2
deps/undici/src/types/client.d.ts
vendored
2
deps/undici/src/types/client.d.ts
vendored
|
@ -71,8 +71,6 @@ export declare namespace Client {
|
|||
/** TODO */
|
||||
maxCachedSessions?: number;
|
||||
/** TODO */
|
||||
maxRedirections?: number;
|
||||
/** TODO */
|
||||
connect?: Partial<buildConnector.BuildOptions> | buildConnector.connector;
|
||||
/** TODO */
|
||||
maxRequestsPerClient?: number;
|
||||
|
|
6
deps/undici/src/types/dispatcher.d.ts
vendored
6
deps/undici/src/types/dispatcher.d.ts
vendored
|
@ -135,8 +135,6 @@ declare namespace Dispatcher {
|
|||
signal?: AbortSignal | EventEmitter | null;
|
||||
/** This argument parameter is passed through to `ConnectData` */
|
||||
opaque?: TOpaque;
|
||||
/** Default: 0 */
|
||||
maxRedirections?: number;
|
||||
/** Default: false */
|
||||
redirectionLimitReached?: boolean;
|
||||
/** Default: `null` */
|
||||
|
@ -147,8 +145,6 @@ declare namespace Dispatcher {
|
|||
opaque?: TOpaque;
|
||||
/** Default: `null` */
|
||||
signal?: AbortSignal | EventEmitter | null;
|
||||
/** Default: 0 */
|
||||
maxRedirections?: number;
|
||||
/** Default: false */
|
||||
redirectionLimitReached?: boolean;
|
||||
/** Default: `null` */
|
||||
|
@ -172,8 +168,6 @@ declare namespace Dispatcher {
|
|||
protocol?: string;
|
||||
/** Default: `null` */
|
||||
signal?: AbortSignal | EventEmitter | null;
|
||||
/** Default: 0 */
|
||||
maxRedirections?: number;
|
||||
/** Default: false */
|
||||
redirectionLimitReached?: boolean;
|
||||
/** Default: `null` */
|
||||
|
|
2
deps/undici/src/types/h2c-client.d.ts
vendored
2
deps/undici/src/types/h2c-client.d.ts
vendored
|
@ -51,8 +51,6 @@ export declare namespace H2CClient {
|
|||
/** TODO */
|
||||
maxCachedSessions?: number;
|
||||
/** TODO */
|
||||
maxRedirections?: number;
|
||||
/** TODO */
|
||||
connect?: Omit<Partial<buildConnector.BuildOptions>, 'allowH2'> | buildConnector.connector;
|
||||
/** TODO */
|
||||
maxRequestsPerClient?: number;
|
||||
|
|
4
deps/undici/src/types/index.d.ts
vendored
4
deps/undici/src/types/index.d.ts
vendored
|
@ -13,6 +13,7 @@ import Agent from './agent'
|
|||
import MockClient from './mock-client'
|
||||
import MockPool from './mock-pool'
|
||||
import MockAgent from './mock-agent'
|
||||
import { SnapshotAgent } from './snapshot-agent'
|
||||
import { MockCallHistory, MockCallHistoryLog } from './mock-call-history'
|
||||
import mockErrors from './mock-errors'
|
||||
import ProxyAgent from './proxy-agent'
|
||||
|
@ -33,7 +34,7 @@ export * from './content-type'
|
|||
export * from './cache'
|
||||
export { Interceptable } from './mock-interceptor'
|
||||
|
||||
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient }
|
||||
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient }
|
||||
export default Undici
|
||||
|
||||
declare namespace Undici {
|
||||
|
@ -58,6 +59,7 @@ declare namespace Undici {
|
|||
const MockClient: typeof import('./mock-client').default
|
||||
const MockPool: typeof import('./mock-pool').default
|
||||
const MockAgent: typeof import('./mock-agent').default
|
||||
const SnapshotAgent: typeof import('./snapshot-agent').SnapshotAgent
|
||||
const MockCallHistory: typeof import('./mock-call-history').MockCallHistory
|
||||
const MockCallHistoryLog: typeof import('./mock-call-history').MockCallHistoryLog
|
||||
const mockErrors: typeof import('./mock-errors').default
|
||||
|
|
1
deps/undici/src/types/mock-interceptor.d.ts
vendored
1
deps/undici/src/types/mock-interceptor.d.ts
vendored
|
@ -69,7 +69,6 @@ declare namespace MockInterceptor {
|
|||
headers?: Headers | Record<string, string>;
|
||||
origin?: string;
|
||||
body?: BodyInit | Dispatcher.DispatchOptions['body'] | null;
|
||||
maxRedirections?: number;
|
||||
}
|
||||
|
||||
export type MockResponseDataHandler<TData extends object = object> = (
|
||||
|
|
107
deps/undici/src/types/snapshot-agent.d.ts
vendored
Normal file
107
deps/undici/src/types/snapshot-agent.d.ts
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
import MockAgent from './mock-agent'
|
||||
|
||||
declare class SnapshotRecorder {
|
||||
constructor (options?: SnapshotRecorder.Options)
|
||||
|
||||
record (requestOpts: any, response: any): Promise<void>
|
||||
findSnapshot (requestOpts: any): SnapshotRecorder.Snapshot | undefined
|
||||
loadSnapshots (filePath?: string): Promise<void>
|
||||
saveSnapshots (filePath?: string): Promise<void>
|
||||
clear (): void
|
||||
getSnapshots (): SnapshotRecorder.Snapshot[]
|
||||
size (): number
|
||||
resetCallCounts (): void
|
||||
deleteSnapshot (requestOpts: any): boolean
|
||||
getSnapshotInfo (requestOpts: any): SnapshotRecorder.SnapshotInfo | null
|
||||
replaceSnapshots (snapshotData: SnapshotRecorder.SnapshotData[]): void
|
||||
destroy (): void
|
||||
}
|
||||
|
||||
declare namespace SnapshotRecorder {
|
||||
export interface Options {
|
||||
snapshotPath?: string
|
||||
mode?: 'record' | 'playback' | 'update'
|
||||
maxSnapshots?: number
|
||||
autoFlush?: boolean
|
||||
flushInterval?: number
|
||||
matchHeaders?: string[]
|
||||
ignoreHeaders?: string[]
|
||||
excludeHeaders?: string[]
|
||||
matchBody?: boolean
|
||||
matchQuery?: boolean
|
||||
caseSensitive?: boolean
|
||||
shouldRecord?: (requestOpts: any) => boolean
|
||||
shouldPlayback?: (requestOpts: any) => boolean
|
||||
excludeUrls?: (string | RegExp)[]
|
||||
}
|
||||
|
||||
export interface Snapshot {
|
||||
request: {
|
||||
method: string
|
||||
url: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
}
|
||||
responses: {
|
||||
statusCode: number
|
||||
headers: Record<string, string>
|
||||
body: string
|
||||
trailers: Record<string, string>
|
||||
}[]
|
||||
callCount: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SnapshotInfo {
|
||||
hash: string
|
||||
request: {
|
||||
method: string
|
||||
url: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
}
|
||||
responseCount: number
|
||||
callCount: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SnapshotData {
|
||||
hash: string
|
||||
snapshot: Snapshot
|
||||
}
|
||||
}
|
||||
|
||||
declare class SnapshotAgent extends MockAgent {
|
||||
constructor (options?: SnapshotAgent.Options)
|
||||
|
||||
saveSnapshots (filePath?: string): Promise<void>
|
||||
loadSnapshots (filePath?: string): Promise<void>
|
||||
getRecorder (): SnapshotRecorder
|
||||
getMode (): 'record' | 'playback' | 'update'
|
||||
clearSnapshots (): void
|
||||
resetCallCounts (): void
|
||||
deleteSnapshot (requestOpts: any): boolean
|
||||
getSnapshotInfo (requestOpts: any): SnapshotRecorder.SnapshotInfo | null
|
||||
replaceSnapshots (snapshotData: SnapshotRecorder.SnapshotData[]): void
|
||||
}
|
||||
|
||||
declare namespace SnapshotAgent {
|
||||
export interface Options extends MockAgent.Options {
|
||||
mode?: 'record' | 'playback' | 'update'
|
||||
snapshotPath?: string
|
||||
maxSnapshots?: number
|
||||
autoFlush?: boolean
|
||||
flushInterval?: number
|
||||
matchHeaders?: string[]
|
||||
ignoreHeaders?: string[]
|
||||
excludeHeaders?: string[]
|
||||
matchBody?: boolean
|
||||
matchQuery?: boolean
|
||||
caseSensitive?: boolean
|
||||
shouldRecord?: (requestOpts: any) => boolean
|
||||
shouldPlayback?: (requestOpts: any) => boolean
|
||||
excludeUrls?: (string | RegExp)[]
|
||||
}
|
||||
}
|
||||
|
||||
export { SnapshotAgent, SnapshotRecorder }
|
149
deps/undici/undici.js
vendored
149
deps/undici/undici.js
vendored
|
@ -1019,7 +1019,6 @@ var require_util = __commonJS({
|
|||
var { IncomingMessage } = require("node:http");
|
||||
var stream = require("node:stream");
|
||||
var net = require("node:net");
|
||||
var { Blob: Blob2 } = require("node:buffer");
|
||||
var { stringify } = require("node:querystring");
|
||||
var { EventEmitter: EE } = require("node:events");
|
||||
var timers = require_timers();
|
||||
|
@ -1074,7 +1073,7 @@ var require_util = __commonJS({
|
|||
function isBlobLike(object) {
|
||||
if (object === null) {
|
||||
return false;
|
||||
} else if (object instanceof Blob2) {
|
||||
} else if (object instanceof Blob) {
|
||||
return true;
|
||||
} else if (typeof object !== "object") {
|
||||
return false;
|
||||
|
@ -4365,7 +4364,7 @@ var require_webidl = __commonJS({
|
|||
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream);
|
||||
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob);
|
||||
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams);
|
||||
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require("node:buffer").File);
|
||||
webidl.is.File = webidl.util.MakeTypeAssertion(File);
|
||||
webidl.is.URL = webidl.util.MakeTypeAssertion(URL);
|
||||
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal);
|
||||
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort);
|
||||
|
@ -5472,9 +5471,7 @@ var require_formdata = __commonJS({
|
|||
var { iteratorMixin } = require_util2();
|
||||
var { kEnumerableProperty } = require_util();
|
||||
var { webidl } = require_webidl();
|
||||
var { File: NativeFile } = require("node:buffer");
|
||||
var nodeUtil = require("node:util");
|
||||
var File = globalThis.File ?? NativeFile;
|
||||
var FormData = class _FormData {
|
||||
static {
|
||||
__name(this, "FormData");
|
||||
|
@ -5643,8 +5640,6 @@ var require_formdata_parser = __commonJS({
|
|||
var { makeEntry } = require_formdata();
|
||||
var { webidl } = require_webidl();
|
||||
var assert = require("node:assert");
|
||||
var { File: NodeFile } = require("node:buffer");
|
||||
var File = globalThis.File ?? NodeFile;
|
||||
var formDataNameBuffer = Buffer.from('form-data; name="');
|
||||
var filenameBuffer = Buffer.from("filename");
|
||||
var dd = Buffer.from("--");
|
||||
|
@ -5946,7 +5941,6 @@ var require_body = __commonJS({
|
|||
} = require_util2();
|
||||
var { FormData, setFormDataState } = require_formdata();
|
||||
var { webidl } = require_webidl();
|
||||
var { Blob: Blob2 } = require("node:buffer");
|
||||
var assert = require("node:assert");
|
||||
var { isErrored, isDisturbed } = require("node:stream");
|
||||
var { isArrayBuffer } = require("node:util/types");
|
||||
|
@ -6141,7 +6135,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r
|
|||
} else if (mimeType) {
|
||||
mimeType = serializeAMimeType(mimeType);
|
||||
}
|
||||
return new Blob2([bytes], { type: mimeType });
|
||||
return new Blob([bytes], { type: mimeType });
|
||||
}, instance, getInternalState);
|
||||
},
|
||||
arrayBuffer() {
|
||||
|
@ -8787,7 +8781,7 @@ var require_global2 = __commonJS({
|
|||
var require_proxy_agent = __commonJS({
|
||||
"lib/dispatcher/proxy-agent.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var { kProxy, kClose, kDestroy, kDispatch, kConnector } = require_symbols();
|
||||
var { kProxy, kClose, kDestroy, kDispatch } = require_symbols();
|
||||
var { URL: URL2 } = require("node:url");
|
||||
var Agent = require_agent();
|
||||
var Pool = require_pool();
|
||||
|
@ -8812,56 +8806,59 @@ var require_proxy_agent = __commonJS({
|
|||
__name(defaultFactory, "defaultFactory");
|
||||
var noop = /* @__PURE__ */ __name(() => {
|
||||
}, "noop");
|
||||
var ProxyClient = class extends DispatcherBase {
|
||||
function defaultAgentFactory(origin, opts) {
|
||||
if (opts.connections === 1) {
|
||||
return new Client(origin, opts);
|
||||
}
|
||||
return new Pool(origin, opts);
|
||||
}
|
||||
__name(defaultAgentFactory, "defaultAgentFactory");
|
||||
var Http1ProxyWrapper = class extends DispatcherBase {
|
||||
static {
|
||||
__name(this, "ProxyClient");
|
||||
}
|
||||
#client = null;
|
||||
constructor(origin, opts) {
|
||||
if (typeof origin === "string") {
|
||||
origin = new URL2(origin);
|
||||
}
|
||||
if (origin.protocol !== "http:" && origin.protocol !== "https:") {
|
||||
throw new InvalidArgumentError("ProxyClient only supports http and https protocols");
|
||||
__name(this, "Http1ProxyWrapper");
|
||||
}
|
||||
#client;
|
||||
constructor(proxyUrl, { headers = {}, connect, factory }) {
|
||||
super();
|
||||
this.#client = new Client(origin, opts);
|
||||
if (!proxyUrl) {
|
||||
throw new InvalidArgumentError("Proxy URL is mandatory");
|
||||
}
|
||||
async [kClose]() {
|
||||
await this.#client.close();
|
||||
}
|
||||
async [kDestroy]() {
|
||||
await this.#client.destroy();
|
||||
}
|
||||
async [kDispatch](opts, handler) {
|
||||
const { method, origin } = opts;
|
||||
if (method === "CONNECT") {
|
||||
this.#client[kConnector](
|
||||
{
|
||||
origin,
|
||||
port: opts.port || defaultProtocolPort(opts.protocol),
|
||||
path: opts.host,
|
||||
signal: opts.signal,
|
||||
headers: {
|
||||
...this[kProxyHeaders],
|
||||
host: opts.host
|
||||
},
|
||||
servername: this[kProxyTls]?.servername || opts.servername
|
||||
},
|
||||
(err, socket) => {
|
||||
if (err) {
|
||||
handler.callback(err);
|
||||
this[kProxyHeaders] = headers;
|
||||
if (factory) {
|
||||
this.#client = factory(proxyUrl, { connect });
|
||||
} else {
|
||||
handler.callback(null, { socket, statusCode: 200 });
|
||||
this.#client = new Client(proxyUrl, { connect });
|
||||
}
|
||||
}
|
||||
);
|
||||
[kDispatch](opts, handler) {
|
||||
const onHeaders = handler.onHeaders;
|
||||
handler.onHeaders = function(statusCode, data, resume) {
|
||||
if (statusCode === 407) {
|
||||
if (typeof handler.onError === "function") {
|
||||
handler.onError(new InvalidArgumentError("Proxy Authentication Required (407)"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof origin === "string") {
|
||||
opts.origin = new URL2(origin);
|
||||
if (onHeaders) onHeaders.call(this, statusCode, data, resume);
|
||||
};
|
||||
const {
|
||||
origin,
|
||||
path = "/",
|
||||
headers = {}
|
||||
} = opts;
|
||||
opts.path = origin + path;
|
||||
if (!("host" in headers) && !("Host" in headers)) {
|
||||
const { host } = new URL2(origin);
|
||||
headers.host = host;
|
||||
}
|
||||
return this.#client.dispatch(opts, handler);
|
||||
opts.headers = { ...this[kProxyHeaders], ...headers };
|
||||
return this.#client[kDispatch](opts, handler);
|
||||
}
|
||||
async [kClose]() {
|
||||
return this.#client.close();
|
||||
}
|
||||
async [kDestroy](err) {
|
||||
return this.#client.destroy(err);
|
||||
}
|
||||
};
|
||||
var ProxyAgent = class extends DispatcherBase {
|
||||
|
@ -8884,6 +8881,7 @@ var require_proxy_agent = __commonJS({
|
|||
this[kRequestTls] = opts.requestTls;
|
||||
this[kProxyTls] = opts.proxyTls;
|
||||
this[kProxyHeaders] = opts.headers || {};
|
||||
this[kTunnelProxy] = proxyTunnel;
|
||||
if (opts.auth && opts.token) {
|
||||
throw new InvalidArgumentError("opts.auth cannot be used in combination with opts.token");
|
||||
} else if (opts.auth) {
|
||||
|
@ -8893,18 +8891,24 @@ var require_proxy_agent = __commonJS({
|
|||
} else if (username && password) {
|
||||
this[kProxyHeaders]["proxy-authorization"] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString("base64")}`;
|
||||
}
|
||||
const factory = !proxyTunnel && protocol === "http:" ? (origin2, options) => {
|
||||
if (origin2.protocol === "http:") {
|
||||
return new ProxyClient(origin2, options);
|
||||
}
|
||||
return new Client(origin2, options);
|
||||
} : void 0;
|
||||
const connect = buildConnector({ ...opts.proxyTls });
|
||||
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls });
|
||||
this[kClient] = clientFactory(url, { connect, factory });
|
||||
this[kTunnelProxy] = proxyTunnel;
|
||||
const agentFactory = opts.factory || defaultAgentFactory;
|
||||
const factory = /* @__PURE__ */ __name((origin2, options) => {
|
||||
const { protocol: protocol2 } = new URL2(origin2);
|
||||
if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") {
|
||||
return new Http1ProxyWrapper(this[kProxy].uri, {
|
||||
headers: this[kProxyHeaders],
|
||||
connect,
|
||||
factory: agentFactory
|
||||
});
|
||||
}
|
||||
return agentFactory(origin2, options);
|
||||
}, "factory");
|
||||
this[kClient] = clientFactory(url, { connect });
|
||||
this[kAgent] = new Agent({
|
||||
...opts,
|
||||
factory,
|
||||
connect: /* @__PURE__ */ __name(async (opts2, callback) => {
|
||||
let requestedPath = opts2.host;
|
||||
if (!opts2.port) {
|
||||
|
@ -8955,9 +8959,6 @@ var require_proxy_agent = __commonJS({
|
|||
const { host } = new URL2(opts.origin);
|
||||
headers.host = host;
|
||||
}
|
||||
if (!this.#shouldConnect(new URL2(opts.origin))) {
|
||||
opts.path = opts.origin + opts.path;
|
||||
}
|
||||
return this[kAgent].dispatch(
|
||||
{
|
||||
...opts,
|
||||
|
@ -8987,18 +8988,6 @@ var require_proxy_agent = __commonJS({
|
|||
await this[kAgent].destroy();
|
||||
await this[kClient].destroy();
|
||||
}
|
||||
#shouldConnect(uri) {
|
||||
if (typeof uri === "string") {
|
||||
uri = new URL2(uri);
|
||||
}
|
||||
if (this[kTunnelProxy]) {
|
||||
return true;
|
||||
}
|
||||
if (uri.protocol !== "http:" || this[kProxy].protocol !== "http:") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
function buildHeaders(headers) {
|
||||
if (Array.isArray(headers)) {
|
||||
|
@ -14273,7 +14262,7 @@ var require_readable = __commonJS({
|
|||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {string|symbol} event
|
||||
* @param {(...args: any[]) => void} listener
|
||||
* @returns {this}
|
||||
*/
|
||||
|
@ -14285,7 +14274,7 @@ var require_readable = __commonJS({
|
|||
return super.on(event, listener);
|
||||
}
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {string|symbol} event
|
||||
* @param {(...args: any[]) => void} listener
|
||||
* @returns {this}
|
||||
*/
|
||||
|
@ -14317,11 +14306,13 @@ var require_readable = __commonJS({
|
|||
* @returns {boolean}
|
||||
*/
|
||||
push(chunk) {
|
||||
this[kBytesRead] += chunk ? chunk.length : 0;
|
||||
if (this[kConsume] && chunk !== null) {
|
||||
if (chunk) {
|
||||
this[kBytesRead] += chunk.length;
|
||||
if (this[kConsume]) {
|
||||
consumePush(this[kConsume], chunk);
|
||||
return this[kReading] ? super.push(chunk) : true;
|
||||
}
|
||||
}
|
||||
return super.push(chunk);
|
||||
}
|
||||
/**
|
||||
|
@ -14473,9 +14464,7 @@ var require_readable = __commonJS({
|
|||
if (isUnusable(stream)) {
|
||||
const rState = stream._readableState;
|
||||
if (rState.destroyed && rState.closeEmitted === false) {
|
||||
stream.on("error", (err) => {
|
||||
reject(err);
|
||||
}).on("close", () => {
|
||||
stream.on("error", reject).on("close", () => {
|
||||
reject(new TypeError("unusable"));
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
// Refer to tools/dep_updaters/update-undici.sh
|
||||
#ifndef SRC_UNDICI_VERSION_H_
|
||||
#define SRC_UNDICI_VERSION_H_
|
||||
#define UNDICI_VERSION "7.12.0"
|
||||
#define UNDICI_VERSION "7.13.0"
|
||||
#endif // SRC_UNDICI_VERSION_H_
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue