import { __assign } from "tslib";
import { equal } from "@wry/equality";
import { createFulfilledPromise, createRejectedPromise, } from "../../../utilities/index.js";
import { wrapPromiseWithState } from "../../../utilities/index.js";
var QUERY_REFERENCE_SYMBOL = Symbol();
var PROMISE_SYMBOL = Symbol();
export function wrapQueryRef(internalQueryRef) {
    var _a;
    var ref = (_a = {
            toPromise: function () {
                // We avoid resolving this promise with the query data because we want to
                // discourage using the server data directly from the queryRef. Instead,
                // the data should be accessed through `useReadQuery`. When the server
                // data is needed, its better to use `client.query()` directly.
                //
                // Here we resolve with the ref itself to make using this in React Router
                // or TanStack Router `loader` functions a bit more ergonomic e.g.
                //
                // function loader() {
                //   return { queryRef: await preloadQuery(query).toPromise() }
                // }
                return getWrappedPromise(ref).then(function () { return ref; });
            }
        },
        _a[QUERY_REFERENCE_SYMBOL] = internalQueryRef,
        _a[PROMISE_SYMBOL] = internalQueryRef.promise,
        _a);
    return ref;
}
export function getWrappedPromise(queryRef) {
    var internalQueryRef = unwrapQueryRef(queryRef);
    return internalQueryRef.promise.status === "fulfilled" ?
        internalQueryRef.promise
        : queryRef[PROMISE_SYMBOL];
}
export function unwrapQueryRef(queryRef) {
    return queryRef[QUERY_REFERENCE_SYMBOL];
}
export function updateWrappedQueryRef(queryRef, promise) {
    queryRef[PROMISE_SYMBOL] = promise;
}
var OBSERVED_CHANGED_OPTIONS = [
    "canonizeResults",
    "context",
    "errorPolicy",
    "fetchPolicy",
    "refetchWritePolicy",
    "returnPartialData",
];
var InternalQueryReference = /** @class */ (function () {
    function InternalQueryReference(observable, options) {
        var _this = this;
        this.key = {};
        this.listeners = new Set();
        this.references = 0;
        this.handleNext = this.handleNext.bind(this);
        this.handleError = this.handleError.bind(this);
        this.dispose = this.dispose.bind(this);
        this.observable = observable;
        if (options.onDispose) {
            this.onDispose = options.onDispose;
        }
        this.setResult();
        this.subscribeToQuery();
        // Start a timer that will automatically dispose of the query if the
        // suspended resource does not use this queryRef in the given time. This
        // helps prevent memory leaks when a component has unmounted before the
        // query has finished loading.
        var startDisposeTimer = function () {
            var _a;
            if (!_this.references) {
                _this.autoDisposeTimeoutId = setTimeout(_this.dispose, (_a = options.autoDisposeTimeoutMs) !== null && _a !== void 0 ? _a : 30000);
            }
        };
        // We wait until the request has settled to ensure we don't dispose of the
        // query ref before the request finishes, otherwise we would leave the
        // promise in a pending state rendering the suspense boundary indefinitely.
        this.promise.then(startDisposeTimer, startDisposeTimer);
    }
    Object.defineProperty(InternalQueryReference.prototype, "disposed", {
        get: function () {
            return this.subscription.closed;
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(InternalQueryReference.prototype, "watchQueryOptions", {
        get: function () {
            return this.observable.options;
        },
        enumerable: false,
        configurable: true
    });
    InternalQueryReference.prototype.reinitialize = function () {
        var observable = this.observable;
        var originalFetchPolicy = this.watchQueryOptions.fetchPolicy;
        try {
            if (originalFetchPolicy !== "no-cache") {
                observable.resetLastResults();
                observable.silentSetOptions({ fetchPolicy: "cache-first" });
            }
            else {
                observable.silentSetOptions({ fetchPolicy: "standby" });
            }
            this.subscribeToQuery();
            if (originalFetchPolicy === "no-cache") {
                return;
            }
            observable.resetDiff();
            this.setResult();
        }
        finally {
            observable.silentSetOptions({ fetchPolicy: originalFetchPolicy });
        }
    };
    InternalQueryReference.prototype.retain = function () {
        var _this = this;
        this.references++;
        clearTimeout(this.autoDisposeTimeoutId);
        var disposed = false;
        return function () {
            if (disposed) {
                return;
            }
            disposed = true;
            _this.references--;
            // Wait before fully disposing in case the app is running in strict mode.
            setTimeout(function () {
                if (!_this.references) {
                    _this.dispose();
                }
            });
        };
    };
    InternalQueryReference.prototype.didChangeOptions = function (watchQueryOptions) {
        var _this = this;
        return OBSERVED_CHANGED_OPTIONS.some(function (option) {
            return !equal(_this.watchQueryOptions[option], watchQueryOptions[option]);
        });
    };
    InternalQueryReference.prototype.applyOptions = function (watchQueryOptions) {
        var _a = this.watchQueryOptions, currentFetchPolicy = _a.fetchPolicy, currentCanonizeResults = _a.canonizeResults;
        // "standby" is used when `skip` is set to `true`. Detect when we've
        // enabled the query (i.e. `skip` is `false`) to execute a network request.
        if (currentFetchPolicy === "standby" &&
            currentFetchPolicy !== watchQueryOptions.fetchPolicy) {
            this.initiateFetch(this.observable.reobserve(watchQueryOptions));
        }
        else {
            this.observable.silentSetOptions(watchQueryOptions);
            if (currentCanonizeResults !== watchQueryOptions.canonizeResults) {
                this.result = __assign(__assign({}, this.result), this.observable.getCurrentResult());
                this.promise = createFulfilledPromise(this.result);
            }
        }
        return this.promise;
    };
    InternalQueryReference.prototype.listen = function (listener) {
        var _this = this;
        this.listeners.add(listener);
        return function () {
            _this.listeners.delete(listener);
        };
    };
    InternalQueryReference.prototype.refetch = function (variables) {
        return this.initiateFetch(this.observable.refetch(variables));
    };
    InternalQueryReference.prototype.fetchMore = function (options) {
        return this.initiateFetch(this.observable.fetchMore(options));
    };
    InternalQueryReference.prototype.dispose = function () {
        this.subscription.unsubscribe();
        this.onDispose();
    };
    InternalQueryReference.prototype.onDispose = function () {
        // noop. overridable by options
    };
    InternalQueryReference.prototype.handleNext = function (result) {
        var _a;
        switch (this.promise.status) {
            case "pending": {
                // Maintain the last successful `data` value if the next result does not
                // have one.
                if (result.data === void 0) {
                    result.data = this.result.data;
                }
                this.result = result;
                (_a = this.resolve) === null || _a === void 0 ? void 0 : _a.call(this, result);
                break;
            }
            default: {
                // This occurs when switching to a result that is fully cached when this
                // class is instantiated. ObservableQuery will run reobserve when
                // subscribing, which delivers a result from the cache.
                if (result.data === this.result.data &&
                    result.networkStatus === this.result.networkStatus) {
                    return;
                }
                // Maintain the last successful `data` value if the next result does not
                // have one.
                if (result.data === void 0) {
                    result.data = this.result.data;
                }
                this.result = result;
                this.promise = createFulfilledPromise(result);
                this.deliver(this.promise);
                break;
            }
        }
    };
    InternalQueryReference.prototype.handleError = function (error) {
        var _a;
        this.subscription.unsubscribe();
        this.subscription = this.observable.resubscribeAfterError(this.handleNext, this.handleError);
        switch (this.promise.status) {
            case "pending": {
                (_a = this.reject) === null || _a === void 0 ? void 0 : _a.call(this, error);
                break;
            }
            default: {
                this.promise = createRejectedPromise(error);
                this.deliver(this.promise);
            }
        }
    };
    InternalQueryReference.prototype.deliver = function (promise) {
        this.listeners.forEach(function (listener) { return listener(promise); });
    };
    InternalQueryReference.prototype.initiateFetch = function (returnedPromise) {
        var _this = this;
        this.promise = this.createPendingPromise();
        this.promise.catch(function () { });
        // If the data returned from the fetch is deeply equal to the data already
        // in the cache, `handleNext` will not be triggered leaving the promise we
        // created in a pending state forever. To avoid this situtation, we attempt
        // to resolve the promise if `handleNext` hasn't been run to ensure the
        // promise is resolved correctly.
        returnedPromise
            .then(function (result) {
            var _a;
            if (_this.promise.status === "pending") {
                _this.result = result;
                (_a = _this.resolve) === null || _a === void 0 ? void 0 : _a.call(_this, result);
            }
        })
            .catch(function () { });
        return returnedPromise;
    };
    InternalQueryReference.prototype.subscribeToQuery = function () {
        var _this = this;
        this.subscription = this.observable
            .filter(function (result) { return !equal(result.data, {}) && !equal(result, _this.result); })
            .subscribe(this.handleNext, this.handleError);
    };
    InternalQueryReference.prototype.setResult = function () {
        // Don't save this result as last result to prevent delivery of last result
        // when first subscribing
        var result = this.observable.getCurrentResult(false);
        if (equal(result, this.result)) {
            return;
        }
        this.result = result;
        this.promise =
            (result.data &&
                (!result.partial || this.watchQueryOptions.returnPartialData)) ?
                createFulfilledPromise(result)
                : this.createPendingPromise();
    };
    InternalQueryReference.prototype.createPendingPromise = function () {
        var _this = this;
        return wrapPromiseWithState(new Promise(function (resolve, reject) {
            _this.resolve = resolve;
            _this.reject = reject;
        }));
    };
    return InternalQueryReference;
}());
export { InternalQueryReference };
//# sourceMappingURL=QueryReference.js.map