diff --git a/lib/collectors/DDPQueueCollector.js b/lib/collectors/DDPQueueCollector.js index b817ef8..1341bc4 100644 --- a/lib/collectors/DDPQueueCollector.js +++ b/lib/collectors/DDPQueueCollector.js @@ -14,6 +14,7 @@ export default class DDPQueueCollector { constructor(options = {}) { this.enabled = options.enabled !== false; this.debug = options.debug || false; + this.unblockDepth = 0; // Store wait lists for messages (who they're waiting for) this.waitLists = {}; @@ -391,7 +392,21 @@ export default class DDPQueueCollector { // itself be a wrapper (from MethodTracer or another layer), and retrying // after a throw can cause infinite recursion across wrapper layers. See #7. if (typeof originalUnblock === 'function') { - originalUnblock(); + // Deep wrapper chains can still overflow the JS call stack even without + // retries. Once nesting gets too deep, defer one hop to break recursion. + if (self.unblockDepth >= 1000) { + queueMicrotask(() => { + originalUnblock(); + }); + return; + } + + self.unblockDepth++; + try { + originalUnblock(); + } finally { + self.unblockDepth--; + } } }; } diff --git a/tests/unit/collectors/DDPQueueCollector.test.js b/tests/unit/collectors/DDPQueueCollector.test.js index febcfc9..266b0a8 100644 --- a/tests/unit/collectors/DDPQueueCollector.test.js +++ b/tests/unit/collectors/DDPQueueCollector.test.js @@ -129,6 +129,28 @@ describe('DDPQueueCollector', function () { // Should not throw expect(() => wrappedUnblock()).to.not.throw(); }); + + it('breaks deep synchronous unblock chains to avoid stack overflow', function (done) { + const deepCollector = new DDPQueueCollector({ enabled: true }); + const session = createMockSession('deep-session'); + const meteorUnblock = sinon.stub(); + + let chained = meteorUnblock; + for (let i = 0; i < 5000; i++) { + chained = deepCollector.wrapUnblock( + session, + { id: `deep-${i}`, msg: 'sub', name: 'assets.assetsInfo' }, + chained + ); + } + + expect(() => chained()).to.not.throw(); + + setTimeout(() => { + expect(meteorUnblock.calledOnce).to.be.true; + done(); + }, 20); + }); }); describe('_wrapSession', function () {