var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Dispatcher_instances, _Dispatcher_meta, _Dispatcher_app, _Dispatcher_appState, _Dispatcher_q, _Dispatcher_rendered, _Dispatcher_rerenderTimeout, _Dispatcher_runtimeDebugLogging, _Dispatcher_paused, _Dispatcher_pausedEffects, _Dispatcher_logLocalErr, _Dispatcher_onEffects, _Dispatcher_onError, _Dispatcher_onPluginCall, _Dispatcher_processEvent, _Dispatcher_renderPost;
import { BlockRenderEventType, RealtimeDefinition } from '@devvit/protos';
import { UIEventScope } from '@devvit/protos/types/devvit/ui/events/v1alpha/event.js';
import { BrowserLiteClient } from '@devvit/runtime-lite/client/BrowserLiteClient.js';
import { isCircuitBreaker } from '@devvit/shared-types/CircuitBreaker.js';
import { Header } from '@devvit/shared-types/Header.js';
import PQueue from 'p-queue';
import { initDevvitGlobal } from '../../types/devvit-global.js';
const DEFAULT_DIMENSIONS = {
    width: 288,
    height: 320,
    scale: 1.0,
    fontScale: 1.0,
};
// Effects that can only be fired as the result of a user action
const userActionEffects = ['showToast', 'showForm', 'navigateToUrl'];
initDevvitGlobal();
/**
 * App render and effect looping logic. Rendering and event processing is as
 * follows:
 *
 * 1. Try the local runtime, if available.
 * 2. If a local runtime or bundle is unavailable, or local execution fails
 *    (including CircuitBreak), try the remote.
 * 3. Report execution errors by callback only. In general, all errors are
 *    attempted to be tolerated.
 *
 * App / render state is always preserved even on error so a buggy app will try
 * not to show any issues. A bundle or runtime can be replaced on the fly with
 * losing state.
 */
export class Dispatcher {
    constructor() {
        _Dispatcher_instances.add(this);
        _Dispatcher_meta.set(this, void 0);
        /** Dimensions that describe layout information for the custom post entry point */
        this.dimensions = DEFAULT_DIMENSIONS;
        /**
         * Bundle currently loaded. Used to test whether failed requests are for the
         * currently loaded bundle or a previous one.
         */
        _Dispatcher_app.set(this, void 0);
        /**
         * App state is never reset in production. Prior remote executions may have
         * occurred. The next loop will replace the state. Do not lose the current
         * state even after a bundle has been downloaded and is ready to load. The
         * only time state should be reinitialized is when a bundle has been
         * _replaced_ such as by devvit-preview.
         */
        _Dispatcher_appState.set(this, {});
        /** No concurrency is used to enforce order. to-do: replace with array? */
        _Dispatcher_q.set(this, new PQueue({ concurrency: 1 }));
        /**
         * True if app has rendered since loaded. Since a successful render isn't
         * known at queue time, it's possible for multiple redundant renders to be
         * enqueued.
         */
        _Dispatcher_rendered.set(this, false);
        _Dispatcher_rerenderTimeout.set(this, void 0);
        _Dispatcher_runtimeDebugLogging.set(this, false);
        /**
         * True if app has been paused. Paused apps don't run effects, rather store them
         * in #pausedEffects for when it has been unpaused.
         * Default is false to allow for initial remote render.
         */
        _Dispatcher_paused.set(this, false);
        /** Store of any effects which queued while the app was paused. */
        _Dispatcher_pausedEffects.set(this, []);
        _Dispatcher_onPluginCall.set(this, (id, serviceName, method, args, meta) => {
            this.onPluginCall?.(id, serviceName, method, args, meta);
            if (serviceName === RealtimeDefinition.fullName &&
                method === RealtimeDefinition.methods.send.name)
                this._local?.onPluginResponse(id, { error: undefined, success: true, value: {} });
        });
    }
    /** Get the current app state with the current post state. */
    get appState() {
        __classPrivateFieldGet(this, _Dispatcher_appState, "f").__postData = { thingId: this.postID, config: this.postConfig };
        return __classPrivateFieldGet(this, _Dispatcher_appState, "f");
    }
    /**
     * Load an app bundle into the local runtime, if available. The bundle is
     * assumed to implement a UIApp.
     */
    async load(app) {
        this.unload();
        __classPrivateFieldSet(this, _Dispatcher_app, app, "f");
        try {
            return await this._local?.loadBundle(app);
        }
        catch (err) {
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onError).call(this, app, 'Error', err);
        }
    }
    set meta(meta) {
        const copy = { ...meta };
        const debug = [];
        for (const [k, v] of Object.entries(devvit?.debug ?? {}))
            debug.push(`${k}=${v}`);
        if (debug.length)
            copy[Header.Debug] = { values: debug };
        __classPrivateFieldSet(this, _Dispatcher_meta, copy, "f");
    }
    queueEvent(ev) {
        const app = __classPrivateFieldGet(this, _Dispatcher_app, "f");
        if (ev.async) {
            // TODO: Add QoS for different types of events on alternate queues. For
            // now, treat alternate queues as having infinite concurrency.
            void __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_processEvent).call(this, app, ev);
        }
        else {
            void __classPrivateFieldGet(this, _Dispatcher_q, "f").add(() => __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_processEvent).call(this, app, ev));
        }
    }
    queueRenderPost(type, id, data) {
        const app = __classPrivateFieldGet(this, _Dispatcher_app, "f");
        void __classPrivateFieldGet(this, _Dispatcher_q, "f").add(() => __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_renderPost).call(this, app, type, id, data));
    }
    get rendered() {
        return __classPrivateFieldGet(this, _Dispatcher_rendered, "f");
    }
    get pausedEffects() {
        return __classPrivateFieldGet(this, _Dispatcher_pausedEffects, "f");
    }
    /**
     * Discard app and render state. This is used indirectly by devvit-preview
     * which frequently loads new bundles.
     */
    resetAppState() {
        __classPrivateFieldSet(this, _Dispatcher_rendered, false, "f");
        __classPrivateFieldSet(this, _Dispatcher_appState, {}, "f");
    }
    set runtimeDebugLogging(on) {
        __classPrivateFieldSet(this, _Dispatcher_runtimeDebugLogging, on, "f");
        if (this._local)
            this._local.runtimeDebugLogging = on;
    }
    /** Set the runtime source code. */
    setLocalSrc(src) {
        this.unload();
        this._local = src
            ? new BrowserLiteClient(src, (type, err) => this.onError?.(type, err), {
                callback: __classPrivateFieldGet(this, _Dispatcher_onPluginCall, "f"),
                plugins: [],
            })
            : undefined;
        if (this._local)
            this._local.runtimeDebugLogging = __classPrivateFieldGet(this, _Dispatcher_runtimeDebugLogging, "f");
    }
    /** Unload the app and reject any open requests. */
    unload() {
        __classPrivateFieldSet(this, _Dispatcher_app, undefined, "f");
        if (__classPrivateFieldGet(this, _Dispatcher_rerenderTimeout, "f") != null)
            clearTimeout(__classPrivateFieldGet(this, _Dispatcher_rerenderTimeout, "f"));
        __classPrivateFieldGet(this, _Dispatcher_q, "f").clear();
        this._local?.quit();
    }
    /** Pause/unpause the app. */
    togglePause(paused) {
        // If unpaused, run any saved effects while the app was paused.
        if (!paused && __classPrivateFieldGet(this, _Dispatcher_pausedEffects, "f").length > 0) {
            const pausedEffects = __classPrivateFieldGet(this, _Dispatcher_pausedEffects, "f");
            __classPrivateFieldSet(this, _Dispatcher_pausedEffects, [], "f");
            pausedEffects.forEach(({ renderType, effects }) => {
                __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onEffects).call(this, renderType, effects);
            });
        }
        __classPrivateFieldSet(this, _Dispatcher_paused, paused, "f");
    }
}
_Dispatcher_meta = new WeakMap(), _Dispatcher_app = new WeakMap(), _Dispatcher_appState = new WeakMap(), _Dispatcher_q = new WeakMap(), _Dispatcher_rendered = new WeakMap(), _Dispatcher_rerenderTimeout = new WeakMap(), _Dispatcher_runtimeDebugLogging = new WeakMap(), _Dispatcher_paused = new WeakMap(), _Dispatcher_pausedEffects = new WeakMap(), _Dispatcher_onPluginCall = new WeakMap(), _Dispatcher_instances = new WeakSet(), _Dispatcher_logLocalErr = function _Dispatcher_logLocalErr(err) {
    if (!devvit?.debug.runtime)
        return;
    if (isCircuitBreaker(err))
        console.debug('[runtime] try remote call; circuit break');
    else
        console.debug('[runtime] try remote call; local failed', err);
    if (!this.remote)
        console.debug('[runtime] call failed; no remote');
}, _Dispatcher_onEffects = function _Dispatcher_onEffects(renderType, effects) {
    for (const effect of effects) {
        if (effect.rerenderUi) {
            if (__classPrivateFieldGet(this, _Dispatcher_rerenderTimeout, "f") != null)
                clearTimeout(__classPrivateFieldGet(this, _Dispatcher_rerenderTimeout, "f"));
            __classPrivateFieldSet(this, _Dispatcher_rerenderTimeout, setTimeout(() => {
                this.queueRenderPost(BlockRenderEventType.RENDER_EFFECT_EVENT, 'rerender-effect', {});
            }, (effect.rerenderUi.delaySeconds ?? 0) * 1000), "f");
        }
        else if (renderType !== BlockRenderEventType.RENDER_USER_ACTION ||
            userActionEffects.some((type) => effect[type])) {
            this.onUserAction?.(effect);
        }
    }
}, _Dispatcher_onError = function _Dispatcher_onError(app, type, err) {
    // Test if error was for a previously loaded bundle. If so, it's a hanging
    // request waiting for the BrowserLiteClient.quit() rejection. We can clear
    // enqueued requests on quit() but not in-flight requests. If app is
    // undefined, no assumptions can be made about the request since it was for
    // remote execution
    if (app && __classPrivateFieldGet(this, _Dispatcher_app, "f") !== app)
        return;
    this.onError?.(type, err);
}, _Dispatcher_processEvent = async function _Dispatcher_processEvent(app, ev) {
    let rsp;
    const req = { event: ev, state: this.appState };
    if (this._local?.ready && ev.scope !== UIEventScope.REMOTE) {
        try {
            rsp = (await this._local.call('HandleUIEvent', req, __classPrivateFieldGet(this, _Dispatcher_meta, "f")));
        }
        catch (err) {
            if (!isCircuitBreaker(err) || !this.remote)
                __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onError).call(this, app, 'Error', err);
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_logLocalErr).call(this, err);
        }
    }
    else if (devvit?.debug.runtime) {
        console.debug(`[runtime] try remote call; ${ev.scope === UIEventScope.REMOTE ? 'local call forbidden' : 'local not ready'}`);
    }
    if (!rsp && ev.scope !== UIEventScope.LOCAL) {
        try {
            rsp = await this.remote?.HandleUIEvent(req, __classPrivateFieldGet(this, _Dispatcher_meta, "f"));
        }
        catch (err) {
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onError).call(this, app, 'Error', err);
            if (devvit?.debug.runtime)
                console.debug('[runtime] remote call failed', err);
        }
    }
    if (rsp) {
        // Can only change state on the main queue.
        if (!ev.async) {
            __classPrivateFieldSet(this, _Dispatcher_appState, rsp.state ?? {}, "f");
        }
        const renderType = ev.realtimeEvent
            ? BlockRenderEventType.RENDER_EFFECT_EVENT
            : BlockRenderEventType.RENDER_USER_ACTION;
        if (__classPrivateFieldGet(this, _Dispatcher_paused, "f")) {
            __classPrivateFieldGet(this, _Dispatcher_pausedEffects, "f").push({ renderType, effects: rsp.effects });
        }
        else {
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onEffects).call(this, renderType, rsp.effects);
        }
    }
    if (devvit?.debug.runtime) {
        if (!this._local?.ready && !this.remote)
            console.debug('[runtime] call failed; no runtimes');
        else if (!this._local?.ready && ev.scope === UIEventScope.LOCAL)
            console.debug('[runtime] call failed; local event but local runtime is unavailable');
        else if (!this.remote && ev.scope === UIEventScope.REMOTE)
            console.debug('[runtime] call failed; remote event but remote runtime is unavailable');
    }
}, _Dispatcher_renderPost = async function _Dispatcher_renderPost(app, type, id, data) {
    const req = {
        blocks: { type, id, data },
        state: this.appState,
        dimensions: this.dimensions,
    };
    let rsp;
    if (this._local?.ready) {
        try {
            rsp = (await this._local.call('RenderPost', req, __classPrivateFieldGet(this, _Dispatcher_meta, "f")));
        }
        catch (err) {
            if (isCircuitBreaker(err) && err.response)
                rsp = err.response;
            if (!isCircuitBreaker(err) || !this.remote)
                __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onError).call(this, app, 'Error', err);
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_logLocalErr).call(this, err);
        }
    }
    else if (devvit?.debug.runtime)
        console.debug('[runtime] try remote call; local not ready');
    if (!rsp) {
        try {
            rsp = await this.remote?.RenderPost(req, __classPrivateFieldGet(this, _Dispatcher_meta, "f"));
        }
        catch (err) {
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onError).call(this, app, 'Error', err);
            if (devvit?.debug.runtime)
                console.debug('[runtime] remote call failed', err);
        }
    }
    if (rsp) {
        __classPrivateFieldSet(this, _Dispatcher_rendered, true, "f");
        if (rsp.state)
            __classPrivateFieldSet(this, _Dispatcher_appState, rsp.state, "f");
        this.onRenderPost?.(req, rsp);
        if (__classPrivateFieldGet(this, _Dispatcher_paused, "f")) {
            __classPrivateFieldGet(this, _Dispatcher_pausedEffects, "f").push({ renderType: req.blocks.type, effects: rsp.effects });
        }
        else {
            __classPrivateFieldGet(this, _Dispatcher_instances, "m", _Dispatcher_onEffects).call(this, req.blocks.type, rsp.effects);
        }
    }
    if (devvit?.debug.runtime && !this._local?.ready && !this.remote)
        console.debug('[runtime] call failed; no runtimes');
};
//# sourceMappingURL=dispatcher.js.map