【问题标题】:How do I prevent a function from being called from within a specific promise?如何防止从特定承诺中调用函数?
【发布时间】:2021-12-20 08:53:17
【问题描述】:

我正在开发一个库,我想阻止用户调用特定函数以防止无限循环。 通常我会这样做:

let preventFooCalls = false;

function fireUserCallbacks() {
    preventFooCalls = true;

    // Fire callbacks of the user here...

    preventFooCalls = false;
}

function foo() {
    if (preventFooCalls) throw Error();

    // Run the content of foo() ...
    // It will probably call fireUserCallbacks() at some point
}

但是,如果fireUserCallbacks 是异步的,则无法使用此方法。它可能会被多次调用,并且使用异步用户回调,preventFooCalls 不能保证具有正确的值。例如:

let preventFooCalls = false;

async function fireUserCallbacks() {
    preventFooCalls = true;

    // Fire callbacks of the user here one of which being:
    await new Promise(r => setTimeout(r, 1000));

    preventFooCalls = false;
}

// Then when doing:
fireUserCallbacks();
foo(); // This will throw even though it's being called from outside fireUserCallbacks()

如何检测代码是否在特定承诺中运行? 我唯一能想到的是new Error().stack,但这听起来很糟糕。


一些上下文

我想要这个的原因是因为我正在开发一个负责加载资源的库的一部分。其中一些资产可能包含其他可能无限递归的资产。为了处理递归,我希望用户调用另一个函数。因此,当用户从 fireUserCallbacks() 回调之一中调用 foo() 时,我想警告他们。虽然这只会在资产实际上包含无限循环时才会成为问题,但我宁愿完全阻止 foo() 的使用,以防止由于无限循环而导致意外挂起。

edit:这是我的实际代码的一个更复杂的示例。我会分享我的实际代码,但这对于这种格式来说实在是太长了,这个例子已经有点太复杂了。

class AssetManager {
  constructor() {
    this.registeredAssetTypes = new Map();
    
    this.availableAssets = new Map();
  }
  
  registerAssetType(typeId, assetTypeConstructor) {
    this.registeredAssetTypes.set(typeId, assetTypeConstructor);
  }
  
  fillAvailableAssets(assetDatas) {
    for (const assetData of assetDatas) {
      const constructor = this.registeredAssetTypes.get(assetData.type);
      const asset = new constructor(assetData.id, assetData.data);
      this.availableAssets.set(assetData.id, asset);
    }
  }
  
  async loadAsset(assetId, recursionTracker = null) {
    // I have some extra code here that makes sure this function will only have
    // a single running instance, but this example is getting way too long already
    const asset = this.availableAssets.get(assetId);
    
    let isRootRecursionTracker = false;
    if (!recursionTracker) {
      isRootRecursionTracker = true;
      recursionTracker = new RecursionTracker(assetId);
    }
    const assetData = await asset.generateAsset(recursionTracker);
    
    if (isRootRecursionTracker) {
      // If this call was made from outside any `generateAsset` implementation,
      // we will wait for all assets to be loaded and put on the right place.
      await recursionTracker.waitForAll();
      
      // Finally we will give the recursionTracker the created root asset,
      // in case any of the sub assets reference the root asset.
      // Note that circular references in any of the sub assets (i.e. not
      // containing the root asset anywhere in the chain) are already taken care of.
      if (recursionTracker.rootLoadingAsset) {
        recursionTracker.rootLoadingAsset.setLoadedAssetData(assetData);
      }
    }
    
    return assetData;
  }
}
const assetManager = new AssetManager();



class RecursionTracker {
  constructor(rootAssetId) {
    this.rootAssetId = rootAssetId;
    
    this.rootLoadingAsset = null;
    this.loadingAssets = new Map();
  }
  
  loadAsset(assetId, cb){
    let loadingAsset = this.loadingAssets.get(assetId);
    if (!loadingAsset) {
      loadingAsset = new LoadingAsset(assetId);
      this.loadingAssets.set(assetId, loadingAsset);
      if (assetId != this.rootAssetId) {
        loadingAsset.startLoading(this);
      } else {
        this.rootLoadingAsset = loadingAsset;
      }
    }
    loadingAsset.onLoad(cb);
  }
  
  async waitForAll() {
    const promises = [];
    for (const loadingAsset of this.loadingAssets.values()) {
      promises.push(loadingAsset.waitForLoad());
    }
    await Promise.all(promises);
  }
}



class LoadingAsset {
  constructor(assetId) {
    this.assetId = assetId;
    
    this.onLoadCbs = new Set();
    this.loadedAssetData = null;
  }
  
  async startLoading(recursionTracker) {
    const loadedAssetData = await assetManager.loadAsset(this.assetId, recursionTracker);
    this.setLoadedAssetData(loadedAssetData);
  }
  
  onLoad(cb) {
    if (this.loadedAssetData) {
      cb(this.loadedAssetData)
    } else {
      this.onLoadCbs.add(cb);
    }
  }
  
  setLoadedAssetData(assetData) {
    this.loadedAssetData = assetData;
    this.onLoadCbs.forEach(cb => cb(assetData));
  }
  
  async waitForLoad() {
    await new Promise(r => this.onLoad(r));
  }
}



class AssetTypeInterface {
  constructor(id, rawAssetData) {
    this.id = id;
    this.rawAssetData = rawAssetData;
  }
  async generateAsset(recursionTracker) {}
}



class AssetTypeFoo extends AssetTypeInterface {  
  async generateAsset(recursionTracker) {
    // This is here just to simulate network traffic, an indexeddb lookup, or any other async operation:
    await new Promise(r => setTimeout(r, 200));
      
    const subAssets = [];
    for (const subAssetId of this.rawAssetData.subAssets) {
      
      
      // This won't work, as it will start waiting for itself to finish:
      // const subAsset = await assetManager.loadAsset(subAssetId);
      // subAssets.push(subAsset);
      
      // So instead we will create a dummy asset:
      const dummyAsset = {}
      const insertionIndex = subAssets.length;
      subAssets[insertionIndex] = dummyAsset;
      // and load the asset with a callback rather than waiting for a promise
      recursionTracker.loadAsset(subAssetId, (loadedAsset) => {
        // since this will be called outside the `generateAsset` function, this won't hang
        subAssets[insertionIndex] = loadedAsset;
      });
    }
    return {
      foo: this.id,
      subAssets,
    }
  }
}
assetManager.registerAssetType("foo", AssetTypeFoo);



class AssetTypeBar extends AssetTypeInterface {
  async generateAsset(recursionTracker) {
    // This is here just to simulate network traffic, an indexeddb lookup, or any other async operation:
    await new Promise(r => setTimeout(r, 200));
    
    // We'll just return a simple object for this one.
    // No recursion here...
    return {
      bar: this.id,
    };
  }
}
assetManager.registerAssetType("bar", AssetTypeBar);



// This is all the raw asset data as stored on the users disk.
// These are not instances of the assets yet, so no circular references yet.
// The assets only reference other assets by their "id"
assetManager.fillAvailableAssets([
  {
    id: "mainAsset",
    type: "foo",
    data: {
      subAssets: ["subAsset1", "subAsset2"]
    }
  },
  {
    id: "subAsset1",
    type: "bar",
    data: {},
  },
  {
    id: "subAsset2",
    type: "foo",
    data: {
      subAssets: ["mainAsset"]
    }
  }
]);

// This sets the loading of the "mainAsset" in motion. It recursively loads
// all referenced assets and finally puts the loaded assets in the right place,
// completing the circle.
(async () => {
  const asset = await assetManager.loadAsset("mainAsset");
  console.log(asset);
})();

【问题讨论】:

  • 如果这是库,就不要暴露 foo 方法吗?
  • @about14sheep 我仍然希望人们能够从fireUserCallbacks() 函数之外调用它。我过度简化了很多事情,但就我而言,foo() 本质上是加载一个资产并在 Promise 中返回它。用户回调表示将原始资产数据解析为可用对象。
  • "其中一些资产可能包含其他可能无限递归的资产。" - 你的意思是资产(直接或间接)包含它们自己?
  • 我创建了一个新示例,因为我的实际代码太长了。这是我能在不使事情过于复杂的情况下最接近真实的东西。
  • @Jespertheend 谢谢,这很有帮助!我已经可以看到两种方法,但不确定它们是否可行,或者该示例是否在简化中丢失了太多。 a) 是否总是rawAssetData.subAssets 持有引用(对于具有子资产的资产类型)? b) 对于每个资产,您创建多个对象,特别是 const asset = new constructor(assetData.id, assetData.data);const assetData = await asset.generateAsset(recursionTracker); - 您可以将它们合并到使用 new 创建并通过 .generate() 方法“填充”的单个实例中吗?

标签: javascript recursion promise


【解决方案1】:

维护一个队列和一个集合。队列包含待处理的请求。该集合包含待处理的请求、正在进行的请求和成功完成的请求。 (每个项目都包括请求本身;请求的状态:待处理、处理中、完成;可能还有重试计数器。)

当请求发出时,检查它是否在集合中。如果它在集合中,则它已被请求并将被处理、正在处理或已成功处理并且已经可用。如果不在集合中,则将其添加到集合和队列中,然后触发队列处理。如果队列处理已在运行,则忽略触发器。如果不是,则开始队列处理。

队列处理将请求从队列中一一拉出并处理它们。如果请求失败,则可以将其放回队列以重复尝试(可以在项目中包含一个计数器以限制重试),也可以将其从集合中删除,以便以后再次请求。当队列为空时,队列处理结束。

这避免了递归和不必要的重复请求。

【讨论】:

  • 或者每个请求只存储一个promise,不要自己跟踪“请求状态”,让异步函数自动执行“队列处理”。
  • @Bergi 您如何使用该策略实施速率限制? (不是问题的一部分,只是对我自己的东西感兴趣。)
  • 速率限制将需要某种排队,但仍有some elegant 解决方案不保留和明确“处理”队列
  • 谢谢!这是我目前的设置。我在原始答案中创建了一个额外的示例,它更接近我的真实代码。在此示例中,RecursionTracker 是您正在谈论的队列。问题是,我想强制使用 RecursionTracker,但仅在从 generateAsset() 内部调用时。 IE。我想防止在该函数中使用assetManager.loadAsset
猜你喜欢
  • 1970-01-01
  • 2022-01-08
  • 2018-07-06
  • 2017-08-12
  • 1970-01-01
  • 2023-04-09
  • 2015-11-11
  • 2023-03-28
  • 2016-01-11
相关资源
最近更新 更多