"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecursiveDelete = exports.RECURSIVE_DELETE_MIN_PENDING_OPS = exports.RECURSIVE_DELETE_MAX_PENDING_OPS = exports.REFERENCE_NAME_MIN_ID = void 0;
const assert = require("assert");
const _1 = require(".");
const util_1 = require("./util");
const reference_1 = require("./reference");
/*!
 * Datastore allowed numeric IDs where Firestore only allows strings. Numeric
 * IDs are exposed to Firestore as __idNUM__, so this is the lowest possible
 * negative numeric value expressed in that format.
 *
 * This constant is used to specify startAt/endAt values when querying for all
 * descendants in a single collection.
 */
exports.REFERENCE_NAME_MIN_ID = '__id-9223372036854775808__';
/*!
 * The query limit used for recursive deletes when fetching all descendants of
 * the specified reference to delete. This is done to prevent the query stream
 * from streaming documents faster than Firestore can delete.
 */
// Visible for testing.
exports.RECURSIVE_DELETE_MAX_PENDING_OPS = 5000;
/*!
 * The number of pending BulkWriter operations at which RecursiveDelete
 * starts the next limit query to fetch descendants. By starting the query
 * while there are pending operations, Firestore can improve BulkWriter
 * throughput. This helps prevent BulkWriter from idling while Firestore
 * fetches the next query.
 */
exports.RECURSIVE_DELETE_MIN_PENDING_OPS = 1000;
/**
 * Class used to store state required for running a recursive delete operation.
 * Each recursive delete call should use a new instance of the class.
 * @private
 * @internal
 */
class RecursiveDelete {
    /**
     *
     * @param firestore The Firestore instance to use.
     * @param writer The BulkWriter instance to use for delete operations.
     * @param ref The document or collection reference to recursively delete.
     * @param maxLimit The query limit to use when fetching descendants
     * @param minLimit The number of pending BulkWriter operations at which
     * RecursiveDelete starts the next limit query to fetch descendants.
     */
    constructor(firestore, writer, ref, maxLimit, minLimit) {
        this.firestore = firestore;
        this.writer = writer;
        this.ref = ref;
        this.maxLimit = maxLimit;
        this.minLimit = minLimit;
        /**
         * The number of deletes that failed with a permanent error.
         * @private
         * @internal
         */
        this.errorCount = 0;
        /**
         * Whether there are still documents to delete that still need to be fetched.
         * @private
         * @internal
         */
        this.documentsPending = true;
        /**
         * Whether run() has been called.
         * @private
         * @internal
         */
        this.started = false;
        /**
         * A deferred promise that resolves when the recursive delete operation
         * is completed.
         * @private
         * @internal
         */
        this.completionDeferred = new util_1.Deferred();
        /**
         * Whether a query stream is currently in progress. Only one stream can be
         * run at a time.
         * @private
         * @internal
         */
        this.streamInProgress = false;
        /**
         * The number of pending BulkWriter operations. Used to determine when the
         * next query can be run.
         * @private
         * @internal
         */
        this.pendingOpsCount = 0;
        this.errorStack = '';
        this.maxPendingOps = maxLimit;
        this.minPendingOps = minLimit;
    }
    /**
     * Recursively deletes the reference provided in the class constructor.
     * Returns a promise that resolves when all descendants have been deleted, or
     * if an error occurs.
     */
    run() {
        assert(!this.started, 'RecursiveDelete.run() should only be called once.');
        // Capture the error stack to preserve stack tracing across async calls.
        this.errorStack = Error().stack;
        this.writer._verifyNotClosed();
        this.setupStream();
        return this.completionDeferred.promise;
    }
    /**
     * Creates a query stream and attaches event handlers to it.
     * @private
     * @internal
     */
    setupStream() {
        const stream = this.getAllDescendants(this.ref instanceof _1.CollectionReference
            ? this.ref
            : this.ref);
        this.streamInProgress = true;
        let streamedDocsCount = 0;
        stream
            .on('error', err => {
            err.code = 14 /* StatusCode.UNAVAILABLE */;
            err.stack = 'Failed to fetch children documents: ' + err.stack;
            this.lastError = err;
            this.onQueryEnd();
        })
            .on('data', (snap) => {
            streamedDocsCount++;
            this.lastDocumentSnap = snap;
            this.deleteRef(snap.ref);
        })
            .on('end', () => {
            this.streamInProgress = false;
            // If there are fewer than the number of documents specified in the
            // limit() field, we know that the query is complete.
            if (streamedDocsCount < this.minPendingOps) {
                this.onQueryEnd();
            }
            else if (this.pendingOpsCount === 0) {
                this.setupStream();
            }
        });
    }
    /**
     * Retrieves all descendant documents nested under the provided reference.
     * @param ref The reference to fetch all descendants for.
     * @private
     * @internal
     * @return {Stream<QueryDocumentSnapshot>} Stream of descendant documents.
     */
    getAllDescendants(ref) {
        // The parent is the closest ancestor document to the location we're
        // deleting. If we are deleting a document, the parent is the path of that
        // document. If we are deleting a collection, the parent is the path of the
        // document containing that collection (or the database root, if it is a
        // root collection).
        let parentPath = ref._resourcePath;
        if (ref instanceof _1.CollectionReference) {
            parentPath = parentPath.popLast();
        }
        const collectionId = ref instanceof _1.CollectionReference
            ? ref.id
            : ref.parent.id;
        let query = new _1.Query(this.firestore, reference_1.QueryOptions.forKindlessAllDescendants(parentPath, collectionId, 
        /* requireConsistency= */ false));
        // Query for names only to fetch empty snapshots.
        query = query.select(_1.FieldPath.documentId()).limit(this.maxPendingOps);
        if (ref instanceof _1.CollectionReference) {
            // To find all descendants of a collection reference, we need to use a
            // composite filter that captures all documents that start with the
            // collection prefix. The MIN_KEY constant represents the minimum key in
            // this collection, and a null byte + the MIN_KEY represents the minimum
            // key is the next possible collection.
            const nullChar = String.fromCharCode(0);
            const startAt = collectionId + '/' + exports.REFERENCE_NAME_MIN_ID;
            const endAt = collectionId + nullChar + '/' + exports.REFERENCE_NAME_MIN_ID;
            query = query
                .where(_1.FieldPath.documentId(), '>=', startAt)
                .where(_1.FieldPath.documentId(), '<', endAt);
        }
        if (this.lastDocumentSnap) {
            query = query.startAfter(this.lastDocumentSnap);
        }
        return query.stream();
    }
    /**
     * Called when all descendants of the provided reference have been streamed
     * or if a permanent error occurs during the stream. Deletes the developer
     * provided reference and wraps any errors that occurred.
     * @private
     * @internal
     */
    onQueryEnd() {
        this.documentsPending = false;
        if (this.ref instanceof _1.DocumentReference) {
            this.writer.delete(this.ref).catch(err => this.incrementErrorCount(err));
        }
        this.writer.flush().then(async () => {
            var _a;
            if (this.lastError === undefined) {
                this.completionDeferred.resolve();
            }
            else {
                let error = new (require('google-gax/build/src/fallback').GoogleError)(`${this.errorCount} ` +
                    `${this.errorCount !== 1 ? 'deletes' : 'delete'} ` +
                    'failed. The last delete failed with: ');
                if (this.lastError.code !== undefined) {
                    error.code = this.lastError.code;
                }
                error = (0, util_1.wrapError)(error, this.errorStack);
                // Wrap the BulkWriter error last to provide the full stack trace.
                this.completionDeferred.reject(this.lastError.stack
                    ? (0, util_1.wrapError)(error, (_a = this.lastError.stack) !== null && _a !== void 0 ? _a : '')
                    : error);
            }
        });
    }
    /**
     * Deletes the provided reference and starts the next stream if conditions
     * are met.
     * @private
     * @internal
     */
    deleteRef(docRef) {
        this.pendingOpsCount++;
        this.writer
            .delete(docRef)
            .catch(err => {
            this.incrementErrorCount(err);
        })
            .then(() => {
            this.pendingOpsCount--;
            // We wait until the previous stream has ended in order to sure the
            // startAfter document is correct. Starting the next stream while
            // there are pending operations allows Firestore to maximize
            // BulkWriter throughput.
            if (this.documentsPending &&
                !this.streamInProgress &&
                this.pendingOpsCount < this.minPendingOps) {
                this.setupStream();
            }
        });
    }
    incrementErrorCount(err) {
        this.errorCount++;
        this.lastError = err;
    }
}
exports.RecursiveDelete = RecursiveDelete;
//# sourceMappingURL=recursive-delete.js.map