/**
 * This class is used to detect when all cache reads for a given render are settled.
 * We do this to allow for cache warming the prerender without having to continue rendering
 * the remainder of the page. This feature is really only useful when the cacheComponents flag is on
 * and should only be used in codepaths gated with this feature.
 */ import { InvariantError } from '../../shared/lib/invariant-error';
export class CacheSignal {
    constructor(){
        this.count = 0;
        this.earlyListeners = [];
        this.listeners = [];
        this.tickPending = false;
        this.pendingTimeoutCleanup = null;
        this.subscribedSignals = null;
        this.invokeListenersIfNoPendingReads = ()=>{
            this.pendingTimeoutCleanup = null;
            if (this.count === 0) {
                for(let i = 0; i < this.listeners.length; i++){
                    this.listeners[i]();
                }
                this.listeners.length = 0;
            }
        };
        if (process.env.NEXT_RUNTIME === 'edge') {
            // we rely on `process.nextTick`, which is not supported in edge
            throw Object.defineProperty(new InvariantError('CacheSignal cannot be used in the edge runtime, because `cacheComponents` does not support it.'), "__NEXT_ERROR_CODE", {
                value: "E728",
                enumerable: false,
                configurable: true
            });
        }
    }
    noMorePendingCaches() {
        if (!this.tickPending) {
            this.tickPending = true;
            queueMicrotask(()=>process.nextTick(()=>{
                    this.tickPending = false;
                    if (this.count === 0) {
                        for(let i = 0; i < this.earlyListeners.length; i++){
                            this.earlyListeners[i]();
                        }
                        this.earlyListeners.length = 0;
                    }
                }));
        }
        // After a cache resolves, React will schedule new rendering work:
        // - in a microtask (when prerendering)
        // - in setImmediate (when rendering)
        // To cover both of these, we have to make sure that we let immediates execute at least once after each cache resolved.
        // We don't know when the pending timeout was scheduled (and if it's about to resolve),
        // so by scheduling a new one, we can be sure that we'll go around the event loop at least once.
        if (this.pendingTimeoutCleanup) {
            // We cancel the timeout in beginRead, so this shouldn't ever be active here,
            // but we still cancel it defensively.
            this.pendingTimeoutCleanup();
        }
        this.pendingTimeoutCleanup = scheduleImmediateAndTimeoutWithCleanup(this.invokeListenersIfNoPendingReads);
    }
    /**
   * This promise waits until there are no more in progress cache reads but no later.
   * This allows for adding more cache reads after to delay cacheReady.
   */ inputReady() {
        return new Promise((resolve)=>{
            this.earlyListeners.push(resolve);
            if (this.count === 0) {
                this.noMorePendingCaches();
            }
        });
    }
    /**
   * If there are inflight cache reads this Promise can resolve in a microtask however
   * if there are no inflight cache reads then we wait at least one task to allow initial
   * cache reads to be initiated.
   */ cacheReady() {
        return new Promise((resolve)=>{
            this.listeners.push(resolve);
            if (this.count === 0) {
                this.noMorePendingCaches();
            }
        });
    }
    beginRead() {
        this.count++;
        // There's a new pending cache, so if there's a `noMorePendingCaches` timeout running,
        // we should cancel it.
        if (this.pendingTimeoutCleanup) {
            this.pendingTimeoutCleanup();
            this.pendingTimeoutCleanup = null;
        }
        if (this.subscribedSignals !== null) {
            for (const subscriber of this.subscribedSignals){
                subscriber.beginRead();
            }
        }
    }
    endRead() {
        if (this.count === 0) {
            throw Object.defineProperty(new InvariantError('CacheSignal got more endRead() calls than beginRead() calls'), "__NEXT_ERROR_CODE", {
                value: "E678",
                enumerable: false,
                configurable: true
            });
        }
        // If this is the last read we need to wait a task before we can claim the cache is settled.
        // The cache read will likely ping a Server Component which can read from the cache again and this
        // will play out in a microtask so we need to only resolve pending listeners if we're still at 0
        // after at least one task.
        // We only want one task scheduled at a time so when we hit count 1 we don't decrement the counter immediately.
        // If intervening reads happen before the scheduled task runs they will never observe count 1 preventing reentrency
        this.count--;
        if (this.count === 0) {
            this.noMorePendingCaches();
        }
        if (this.subscribedSignals !== null) {
            for (const subscriber of this.subscribedSignals){
                subscriber.endRead();
            }
        }
    }
    hasPendingReads() {
        return this.count > 0;
    }
    trackRead(promise) {
        this.beginRead();
        // `promise.finally()` still rejects, so don't use it here to avoid unhandled rejections
        const onFinally = this.endRead.bind(this);
        promise.then(onFinally, onFinally);
        return promise;
    }
    subscribeToReads(subscriber) {
        if (subscriber === this) {
            throw Object.defineProperty(new InvariantError('A CacheSignal cannot subscribe to itself'), "__NEXT_ERROR_CODE", {
                value: "E679",
                enumerable: false,
                configurable: true
            });
        }
        if (this.subscribedSignals === null) {
            this.subscribedSignals = new Set();
        }
        this.subscribedSignals.add(subscriber);
        // we'll notify the subscriber of each endRead() on this signal,
        // so we need to give it a corresponding beginRead() for each read we have in flight now.
        for(let i = 0; i < this.count; i++){
            subscriber.beginRead();
        }
        return this.unsubscribeFromReads.bind(this, subscriber);
    }
    unsubscribeFromReads(subscriber) {
        if (!this.subscribedSignals) {
            return;
        }
        this.subscribedSignals.delete(subscriber);
    // we don't need to set the set back to `null` if it's empty --
    // if other signals are subscribing to this one, it'll likely get more subscriptions later,
    // so we'd have to allocate a fresh set again when that happens.
    }
}
function scheduleImmediateAndTimeoutWithCleanup(cb) {
    // If we decide to clean up the timeout, we want to remove
    // either the immediate or the timeout, whichever is still pending.
    let clearPending;
    const immediate = setImmediate(()=>{
        const timeout = setTimeout(cb, 0);
        clearPending = clearTimeout.bind(null, timeout);
    });
    clearPending = clearImmediate.bind(null, immediate);
    return ()=>clearPending();
}

//# sourceMappingURL=cache-signal.js.map