@azure/core-rest-pipeline
Version:
Isomorphic client library for making HTTP requests in node.js and browser.
262 lines • 10.5 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const ValidPhaseNames = new Set(["Deserialize", "Serialize", "Retry", "Sign"]);
/**
* A private implementation of Pipeline.
* Do not export this class from the package.
* @internal
*/
class HttpPipeline {
constructor(policies) {
var _a;
this._policies = [];
this._policies = (_a = policies === null || policies === void 0 ? void 0 : policies.slice(0)) !== null && _a !== void 0 ? _a : [];
this._orderedPolicies = undefined;
}
addPolicy(policy, options = {}) {
if (options.phase && options.afterPhase) {
throw new Error("Policies inside a phase cannot specify afterPhase.");
}
if (options.phase && !ValidPhaseNames.has(options.phase)) {
throw new Error(`Invalid phase name: ${options.phase}`);
}
if (options.afterPhase && !ValidPhaseNames.has(options.afterPhase)) {
throw new Error(`Invalid afterPhase name: ${options.afterPhase}`);
}
this._policies.push({
policy,
options,
});
this._orderedPolicies = undefined;
}
removePolicy(options) {
const removedPolicies = [];
this._policies = this._policies.filter((policyDescriptor) => {
if ((options.name && policyDescriptor.policy.name === options.name) ||
(options.phase && policyDescriptor.options.phase === options.phase)) {
removedPolicies.push(policyDescriptor.policy);
return false;
}
else {
return true;
}
});
this._orderedPolicies = undefined;
return removedPolicies;
}
sendRequest(httpClient, request) {
const policies = this.getOrderedPolicies();
const pipeline = policies.reduceRight((next, policy) => {
return (req) => {
return policy.sendRequest(req, next);
};
}, (req) => httpClient.sendRequest(req));
return pipeline(request);
}
getOrderedPolicies() {
if (!this._orderedPolicies) {
this._orderedPolicies = this.orderPolicies();
}
return this._orderedPolicies;
}
clone() {
return new HttpPipeline(this._policies);
}
static create() {
return new HttpPipeline();
}
orderPolicies() {
/**
* The goal of this method is to reliably order pipeline policies
* based on their declared requirements when they were added.
*
* Order is first determined by phase:
*
* 1. Serialize Phase
* 2. Policies not in a phase
* 3. Deserialize Phase
* 4. Retry Phase
* 5. Sign Phase
*
* Within each phase, policies are executed in the order
* they were added unless they were specified to execute
* before/after other policies or after a particular phase.
*
* To determine the final order, we will walk the policy list
* in phase order multiple times until all dependencies are
* satisfied.
*
* `afterPolicies` are the set of policies that must be
* executed before a given policy. This requirement is
* considered satisfied when each of the listed policies
* have been scheduled.
*
* `beforePolicies` are the set of policies that must be
* executed after a given policy. Since this dependency
* can be expressed by converting it into a equivalent
* `afterPolicies` declarations, they are normalized
* into that form for simplicity.
*
* An `afterPhase` dependency is considered satisfied when all
* policies in that phase have scheduled.
*
*/
const result = [];
// Track all policies we know about.
const policyMap = new Map();
function createPhase(name) {
return {
name,
policies: new Set(),
hasRun: false,
hasAfterPolicies: false,
};
}
// Track policies for each phase.
const serializePhase = createPhase("Serialize");
const noPhase = createPhase("None");
const deserializePhase = createPhase("Deserialize");
const retryPhase = createPhase("Retry");
const signPhase = createPhase("Sign");
// a list of phases in order
const orderedPhases = [serializePhase, noPhase, deserializePhase, retryPhase, signPhase];
// Small helper function to map phase name to each Phase
function getPhase(phase) {
if (phase === "Retry") {
return retryPhase;
}
else if (phase === "Serialize") {
return serializePhase;
}
else if (phase === "Deserialize") {
return deserializePhase;
}
else if (phase === "Sign") {
return signPhase;
}
else {
return noPhase;
}
}
// First walk each policy and create a node to track metadata.
for (const descriptor of this._policies) {
const policy = descriptor.policy;
const options = descriptor.options;
const policyName = policy.name;
if (policyMap.has(policyName)) {
throw new Error("Duplicate policy names not allowed in pipeline");
}
const node = {
policy,
dependsOn: new Set(),
dependants: new Set(),
};
if (options.afterPhase) {
node.afterPhase = getPhase(options.afterPhase);
node.afterPhase.hasAfterPolicies = true;
}
policyMap.set(policyName, node);
const phase = getPhase(options.phase);
phase.policies.add(node);
}
// Now that each policy has a node, connect dependency references.
for (const descriptor of this._policies) {
const { policy, options } = descriptor;
const policyName = policy.name;
const node = policyMap.get(policyName);
if (!node) {
throw new Error(`Missing node for policy ${policyName}`);
}
if (options.afterPolicies) {
for (const afterPolicyName of options.afterPolicies) {
const afterNode = policyMap.get(afterPolicyName);
if (afterNode) {
// Linking in both directions helps later
// when we want to notify dependants.
node.dependsOn.add(afterNode);
afterNode.dependants.add(node);
}
}
}
if (options.beforePolicies) {
for (const beforePolicyName of options.beforePolicies) {
const beforeNode = policyMap.get(beforePolicyName);
if (beforeNode) {
// To execute before another node, make it
// depend on the current node.
beforeNode.dependsOn.add(node);
node.dependants.add(beforeNode);
}
}
}
}
function walkPhase(phase) {
phase.hasRun = true;
// Sets iterate in insertion order
for (const node of phase.policies) {
if (node.afterPhase && (!node.afterPhase.hasRun || node.afterPhase.policies.size)) {
// If this node is waiting on a phase to complete,
// we need to skip it for now.
// Even if the phase is empty, we should wait for it
// to be walked to avoid re-ordering policies.
continue;
}
if (node.dependsOn.size === 0) {
// If there's nothing else we're waiting for, we can
// add this policy to the result list.
result.push(node.policy);
// Notify anything that depends on this policy that
// the policy has been scheduled.
for (const dependant of node.dependants) {
dependant.dependsOn.delete(node);
}
policyMap.delete(node.policy.name);
phase.policies.delete(node);
}
}
}
function walkPhases() {
for (const phase of orderedPhases) {
walkPhase(phase);
// if the phase isn't complete
if (phase.policies.size > 0 && phase !== noPhase) {
if (!noPhase.hasRun) {
// Try running noPhase to see if that unblocks this phase next tick.
// This can happen if a phase that happens before noPhase
// is waiting on a noPhase policy to complete.
walkPhase(noPhase);
}
// Don't proceed to the next phase until this phase finishes.
return;
}
if (phase.hasAfterPolicies) {
// Run any policies unblocked by this phase
walkPhase(noPhase);
}
}
}
// Iterate until we've put every node in the result list.
let iteration = 0;
while (policyMap.size > 0) {
iteration++;
const initialResultLength = result.length;
// Keep walking each phase in order until we can order every node.
walkPhases();
// The result list *should* get at least one larger each time
// after the first full pass.
// Otherwise, we're going to loop forever.
if (result.length <= initialResultLength && iteration > 1) {
throw new Error("Cannot satisfy policy dependencies due to requirements cycle.");
}
}
return result;
}
}
/**
* Creates a totally empty pipeline.
* Useful for testing or creating a custom one.
*/
export function createEmptyPipeline() {
return HttpPipeline.create();
}
//# sourceMappingURL=pipeline.js.map