/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
/**
 * An object which can enter a "broken" state where trying to use it is a UsageError.
 * @remarks
 * Use {@link WithBreakable} to apply this to another object.
 * @sealed
 */
export class Breakable {
    constructor(
    /**
     * A name for a given breakable scope.
     * @remarks
     * This is useful for documenting the semantics of a given Breakable and when inspecting things in the debugger, but is currently otherwise unused.
     */
    name) {
        this.name = name;
    }
    /**
     * Throws if this object is in the broken state.
     * @remarks
     * Can use {@link throwIfBroken} to apply this to a method.
     */
    use() {
        if (this.brokenBy !== undefined) {
            const error = new UsageError(`Invalid use of ${this.name} after it was put into an invalid state by another error.\nOriginal Error:\n${this.brokenBy}`);
            // This "cause" field is added in ES2022, but using if even without that built in support, it is still helpful.
            // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
            // TODO: remove this cast when targeting ES2022 lib or later.
            error.cause =
                this.brokenBy.cause ?? this.brokenBy;
            throw error;
        }
    }
    /**
     * Puts this object into the broken state, and throws an error.
     *
     * @throws If already broken by a different error, throws a UsageError, otherwise throws `brokenBy`.
     */
    break(brokenBy) {
        // If already broken by this error, let it bubble up without rethrowing a modified version.
        // This prevents internal errors like asserts getting rethrown as different errors when wrapped with multiple call to `{@link Breakable.run}` or `{@link breakingMethod}`.
        if (this.brokenBy !== brokenBy) {
            this.use();
            this.brokenBy = brokenBy;
        }
        throw brokenBy;
    }
    /**
     * {@link Breakable.break}, except tolerates `unknown` to be more easily used by catch blocks.
     * @privateRemarks
     * If there is a use-case, this should be made public.
     */
    rethrowCaught(brokenBy) {
        if (brokenBy instanceof Error) {
            this.break(brokenBy);
        }
        this.break(new Error(`Non-error thrown breaking ${this.name}. Thrown value: "${brokenBy}"`));
    }
    /**
     * Runs code which should break the object if it throws.
     * @remarks
     * Like {@link Breakable.use}, this also throws if already broken.
     * Any exceptions this catches are re-thrown.
     * Can use {@link breakingMethod} to apply this to a method.
     */
    run(breaker) {
        this.use();
        try {
            return breaker();
        }
        catch (error) {
            this.rethrowCaught(error);
        }
    }
    /**
     * Clears the existing broken state.
     * @remarks
     * This is rarely safe to to: it is only ok when all objects using this breaker are known to not have been left in an invalid state.
     * This is pretty much only safe in tests which just were checking a specific error was thrown, and which know that error closepath is actually exception safe.
     */
    clearError() {
        assert(this.brokenBy !== undefined, 0x9b6 /* No error to clear */);
        this.brokenBy = undefined;
    }
}
/**
 * Decorator for methods which should break the object when they throw.
 * @remarks
 * This also throws if already broken like {@link throwIfBroken}.
 * See {@link Breakable.run} for details.
 *
 * This should be used on methods which modify data that could result in an unsupported/broken state if an exception is thrown while modifying.
 * It is ok for breakingMethods to call each-other.
 * @privateRemarks
 * Explicitly capturing the full `Target` type is necessary to make this work with generic methods with unknown numbers of type parameters.
 */
export function breakingMethod(target, context) {
    function replacementMethod(...args) {
        if (this.breaker === undefined) {
            // This case is necessary for when wrapping methods which are invoked inside the constructor of the base class before `breaker` is set.
            // Since the constructor throwing does not return an object, failing to put it into a broken state is not too bad.
            // However when more than just the constructed object should be broken, this can result in missing a break.
            return target.call(this, ...args);
        }
        return this.breaker.run(() => {
            return target.call(this, ...args);
        });
    }
    markBreaker(replacementMethod);
    nameFunctionFrom(replacementMethod, target);
    return replacementMethod;
}
/**
 * Decorator for methods which should throw if the object is in a broken state.
 * @remarks
 * This should be used on methods which read data that could be invalid when the object is broken.
 * @privateRemarks
 * Explicitly capturing the full `Target` type is necessary to make this work with generic methods with unknown numbers of type parameters.
 */
export function throwIfBroken(target, context) {
    function replacementMethod(...args) {
        this.breaker.use();
        return target.call(this, ...args);
    }
    markBreaker(replacementMethod);
    nameFunctionFrom(replacementMethod, target);
    return replacementMethod;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function nameFunctionFrom(toName, nameFrom) {
    toName.displayName =
        nameFrom.displayName ?? nameFrom.name;
}
const isBreakerSymbol = Symbol("isBreaker");
// Accepting any function like value is desired and safe here as this does not call the provided function.
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function markBreaker(f) {
    f[isBreakerSymbol] = true;
}
// Accepting any function like value is desired and safe here as this does not call the provided function.
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function isBreaker(f) {
    return isBreakerSymbol in f;
}
/**
 * Decorator for classes which should break when their methods throw.
 * @remarks
 * Applies {@link breakingMethod} to all methods declared directly by class or its base classes.
 * Does not include those on derived classes.
 * Does not include getters or setters, or value properties.
 * Methods already marked as {@link breakingMethod} or {@link throwIfBroken} are unaffected.
 */
export function breakingClass(target, context) {
    // This could extend target, but doing so adds an extra step in the prototype chain and makes the instances just show up as "DecoratedBreakable" in the debugger.
    const DecoratedBreakable = target;
    // Keep track of what keys we have seen (and already wrapped if needed).
    // Used to avoid rewrapping already wrapped properties.
    // Preloaded with "constructor" to avoid wrapping the constructor as there is no need to set the broken flag when the constructor throws and does not return an object.
    // Avoiding wrapping the constructor also avoids messing up the displayed name in the debugger.
    const doNotWrap = new Set(["constructor"]);
    let prototype = target.prototype;
    while (prototype !== null) {
        for (const key of Reflect.ownKeys(prototype)) {
            if (!doNotWrap.has(key)) {
                doNotWrap.add(key);
                const descriptor = Reflect.getOwnPropertyDescriptor(prototype, key);
                // Method
                if (descriptor !== undefined &&
                    typeof descriptor.value === "function" &&
                    !isBreaker(descriptor.value)) {
                    // This does not affect the original class, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
                    const method = descriptor.value;
                    descriptor.value = breakingMethod(method);
                    Object.defineProperty(DecoratedBreakable.prototype, key, descriptor);
                }
            }
        }
        prototype = Reflect.getPrototypeOf(prototype);
    }
    return DecoratedBreakable;
}
//# sourceMappingURL=breakable.js.map