import { InvariantError } from '../../shared/lib/invariant-error';
import { isPrerenderInterruptedError } from './dynamic-rendering';
/**
 * This is a utility function to make scheduling sequential tasks that run back to back easier.
 * We schedule on the same queue (setImmediate) at the same time to ensure no other events can sneak in between.
 */ export function prerenderAndAbortInSequentialTasks(prerender, abort) {
    if (process.env.NEXT_RUNTIME === 'edge') {
        throw Object.defineProperty(new InvariantError('`prerenderAndAbortInSequentialTasks` should not be called in edge runtime.'), "__NEXT_ERROR_CODE", {
            value: "E538",
            enumerable: false
        });
    } else {
        return new Promise((resolve, reject)=>{
            let pendingResult;
            setImmediate(()=>{
                try {
                    pendingResult = prerender();
                    pendingResult.catch(()=>{});
                } catch (err) {
                    reject(err);
                }
            });
            setImmediate(()=>{
                abort();
                resolve(pendingResult);
            });
        });
    }
}
export function prerenderServerWithPhases(signal, render, ...remainingPhases) {
    if (process.env.NEXT_RUNTIME === 'edge') {
        throw Object.defineProperty(new InvariantError('`prerenderAndAbortInSequentialTasks` should not be called in edge runtime.'), "__NEXT_ERROR_CODE", {
            value: "E538",
            enumerable: false
        });
    } else {
        return new Promise((resolve, reject)=>{
            let result;
            signal.addEventListener('abort', ()=>{
                if (isPrerenderInterruptedError(signal.reason)) {
                    result.markInterrupted();
                } else {
                    result.markComplete();
                }
            }, {
                once: true
            });
            setImmediate(()=>{
                try {
                    result = new ServerPrerenderStreamResult(render());
                } catch (err) {
                    reject(err);
                }
            });
            function runFinalTask() {
                try {
                    if (result) {
                        result.markComplete();
                        this();
                    }
                    resolve(result);
                } catch (err) {
                    reject(err);
                }
            }
            function runNextTask() {
                try {
                    if (result) {
                        result.markPhase();
                        this();
                    }
                } catch (err) {
                    reject(err);
                }
            }
            let i = 0;
            for(; i < remainingPhases.length - 1; i++){
                const phase = remainingPhases[i];
                setImmediate(runNextTask.bind(phase));
            }
            if (remainingPhases[i]) {
                const finalPhase = remainingPhases[i];
                setImmediate(runFinalTask.bind(finalPhase));
            }
        });
    }
}
const PENDING = 0;
const COMPLETE = 1;
const INTERRUPTED = 2;
const ERRORED = 3;
export class ServerPrerenderStreamResult {
    constructor(stream){
        this.status = PENDING;
        this.reason = null;
        this.trailingChunks = [];
        this.currentChunks = [];
        this.chunksByPhase = [
            this.currentChunks
        ];
        const reader = stream.getReader();
        const progress = ({ done, value })=>{
            if (done) {
                if (this.status === PENDING) {
                    this.status = COMPLETE;
                }
                return;
            }
            if (this.status === PENDING || this.status === INTERRUPTED) {
                this.currentChunks.push(value);
            } else {
                this.trailingChunks.push(value);
            }
            reader.read().then(progress, error);
        };
        const error = (reason)=>{
            this.status = ERRORED;
            this.reason = reason;
        };
        reader.read().then(progress, error);
    }
    markPhase() {
        this.currentChunks = [];
        this.chunksByPhase.push(this.currentChunks);
    }
    markComplete() {
        if (this.status === PENDING) {
            this.status = COMPLETE;
        }
    }
    markInterrupted() {
        this.status = INTERRUPTED;
    }
    /**
   * Returns a stream which only releases chunks when `releasePhase` is called. This stream will never "complete" because
   * we rely upon the stream remaining open when prerendering to avoid triggering errors for incomplete chunks in the client.
   *
   * asPhasedStream is expected to be called once per result however it is safe to call multiple times as long as we have not
   * transferred the underlying data. Generally this will only happen when streaming to a response
   */ asPhasedStream() {
        switch(this.status){
            case COMPLETE:
            case INTERRUPTED:
                return new PhasedStream(this.chunksByPhase);
            default:
                throw Object.defineProperty(new InvariantError(`ServerPrerenderStreamResult cannot be consumed as a stream because it is not yet complete. status: ${this.status}`), "__NEXT_ERROR_CODE", {
                    value: "E612",
                    enumerable: false
                });
        }
    }
    /**
   * Returns a stream which will release all chunks immediately. This stream will "complete" synchronously. It should be used outside
   * of render use cases like loading client chunks ahead of SSR or writing the streamed content to disk.
   */ asStream() {
        switch(this.status){
            case COMPLETE:
            case INTERRUPTED:
                const chunksByPhase = this.chunksByPhase;
                const trailingChunks = this.trailingChunks;
                return new ReadableStream({
                    start (controller) {
                        for(let i = 0; i < chunksByPhase.length; i++){
                            const chunks = chunksByPhase[i];
                            for(let j = 0; j < chunks.length; j++){
                                controller.enqueue(chunks[j]);
                            }
                        }
                        for(let i = 0; i < trailingChunks.length; i++){
                            controller.enqueue(trailingChunks[i]);
                        }
                        controller.close();
                    }
                });
            default:
                throw Object.defineProperty(new InvariantError(`ServerPrerenderStreamResult cannot be consumed as a stream because it is not yet complete. status: ${this.status}`), "__NEXT_ERROR_CODE", {
                    value: "E612",
                    enumerable: false
                });
        }
    }
}
class PhasedStream extends ReadableStream {
    constructor(chunksByPhase){
        if (chunksByPhase.length === 0) {
            throw Object.defineProperty(new InvariantError('PhasedStream expected at least one phase but none were found.'), "__NEXT_ERROR_CODE", {
                value: "E574",
                enumerable: false
            });
        }
        let destination;
        super({
            start (controller) {
                destination = controller;
            }
        });
        // the start function above is called synchronously during construction so we will always have a destination
        // We wait to assign it until after the super call because we cannot access `this` before calling super
        this.destination = destination;
        this.nextPhase = 0;
        this.chunksByPhase = chunksByPhase;
        this.releasePhase();
    }
    releasePhase() {
        if (this.nextPhase < this.chunksByPhase.length) {
            const chunks = this.chunksByPhase[this.nextPhase++];
            for(let i = 0; i < chunks.length; i++){
                this.destination.enqueue(chunks[i]);
            }
        } else {
            throw Object.defineProperty(new InvariantError('PhasedStream expected more phases to release but none were found.'), "__NEXT_ERROR_CODE", {
                value: "E541",
                enumerable: false
            });
        }
    }
    assertExhausted() {
        if (this.nextPhase < this.chunksByPhase.length) {
            throw Object.defineProperty(new InvariantError('PhasedStream expected no more phases to release but some were found.'), "__NEXT_ERROR_CODE", {
                value: "E584",
                enumerable: false
            });
        }
    }
}
export function prerenderClientWithPhases(render, ...remainingPhases) {
    if (process.env.NEXT_RUNTIME === 'edge') {
        throw Object.defineProperty(new InvariantError('`prerenderAndAbortInSequentialTasks` should not be called in edge runtime.'), "__NEXT_ERROR_CODE", {
            value: "E538",
            enumerable: false
        });
    } else {
        return new Promise((resolve, reject)=>{
            let pendingResult;
            setImmediate(()=>{
                try {
                    pendingResult = render();
                    pendingResult.catch((err)=>reject(err));
                } catch (err) {
                    reject(err);
                }
            });
            function runFinalTask() {
                try {
                    this();
                    resolve(pendingResult);
                } catch (err) {
                    reject(err);
                }
            }
            function runNextTask() {
                try {
                    this();
                } catch (err) {
                    reject(err);
                }
            }
            let i = 0;
            for(; i < remainingPhases.length - 1; i++){
                const phase = remainingPhases[i];
                setImmediate(runNextTask.bind(phase));
            }
            if (remainingPhases[i]) {
                const finalPhase = remainingPhases[i];
                setImmediate(runFinalTask.bind(finalPhase));
            }
        });
    }
}
// React's RSC prerender function will emit an incomplete flight stream when using `prerender`. If the connection
// closes then whatever hanging chunks exist will be errored. This is because prerender (an experimental feature)
// has not yet implemented a concept of resume. For now we will simulate a paused connection by wrapping the stream
// in one that doesn't close even when the underlying is complete.
export class ReactServerResult {
    constructor(stream){
        this._stream = stream;
    }
    tee() {
        if (this._stream === null) {
            throw Object.defineProperty(new Error('Cannot tee a ReactServerResult that has already been consumed'), "__NEXT_ERROR_CODE", {
                value: "E106",
                enumerable: false
            });
        }
        const tee = this._stream.tee();
        this._stream = tee[0];
        return tee[1];
    }
    consume() {
        if (this._stream === null) {
            throw Object.defineProperty(new Error('Cannot consume a ReactServerResult that has already been consumed'), "__NEXT_ERROR_CODE", {
                value: "E470",
                enumerable: false
            });
        }
        const stream = this._stream;
        this._stream = null;
        return stream;
    }
}
export async function createReactServerPrerenderResult(underlying) {
    const chunks = [];
    const { prelude } = await underlying;
    const reader = prelude.getReader();
    while(true){
        const { done, value } = await reader.read();
        if (done) {
            return new ReactServerPrerenderResult(chunks);
        } else {
            chunks.push(value);
        }
    }
}
export async function createReactServerPrerenderResultFromRender(underlying) {
    const chunks = [];
    const reader = underlying.getReader();
    while(true){
        const { done, value } = await reader.read();
        if (done) {
            break;
        } else {
            chunks.push(value);
        }
    }
    return new ReactServerPrerenderResult(chunks);
}
export class ReactServerPrerenderResult {
    assertChunks(expression) {
        if (this._chunks === null) {
            throw Object.defineProperty(new InvariantError(`Cannot \`${expression}\` on a ReactServerPrerenderResult that has already been consumed.`), "__NEXT_ERROR_CODE", {
                value: "E593",
                enumerable: false
            });
        }
        return this._chunks;
    }
    consumeChunks(expression) {
        const chunks = this.assertChunks(expression);
        this.consume();
        return chunks;
    }
    consume() {
        this._chunks = null;
    }
    constructor(chunks){
        this._chunks = chunks;
    }
    asUnclosingStream() {
        const chunks = this.assertChunks('asUnclosingStream()');
        return createUnclosingStream(chunks);
    }
    consumeAsUnclosingStream() {
        const chunks = this.consumeChunks('consumeAsUnclosingStream()');
        return createUnclosingStream(chunks);
    }
    asStream() {
        const chunks = this.assertChunks('asStream()');
        return createClosingStream(chunks);
    }
    consumeAsStream() {
        const chunks = this.consumeChunks('consumeAsStream()');
        return createClosingStream(chunks);
    }
}
function createUnclosingStream(chunks) {
    let i = 0;
    return new ReadableStream({
        async pull (controller) {
            if (i < chunks.length) {
                controller.enqueue(chunks[i++]);
            }
        // we intentionally keep the stream open. The consumer will clear
        // out chunks once finished and the remaining memory will be GC'd
        // when this object goes out of scope
        }
    });
}
function createClosingStream(chunks) {
    let i = 0;
    return new ReadableStream({
        async pull (controller) {
            if (i < chunks.length) {
                controller.enqueue(chunks[i++]);
            } else {
                controller.close();
            }
        }
    });
}

//# sourceMappingURL=app-render-prerender-utils.js.map