var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { jsx as _jsx } from "react/jsx-runtime";
import { mixed } from '@recoiljs/refine';
import React from 'react';
import { atomFamily } from 'recoil';
import { RecoilSync, syncEffect } from 'recoil-sync';
import { validateResponse } from '@darajs/ui-utils';
import { RequestExtrasSerializable, request } from '../../api/http';
import { handleAuthErrors } from '../../auth/auth';
import {} from '../../types';
import { WebSocketCtx } from '../context';
import { StateSynchronizer } from './state-synchronizer';
import { atomFamilyMembersRegistry, atomFamilyRegistry } from './store';
/**
 * Global map to store the extras for each store uid
 */
const STORE_EXTRAS_MAP = new Map();
/**
 * Create a syncEffect for server variable
 *
 * @param variable variable to create the atom effect for
 * @param requestExtras extras object to create the effect for; used to pass through correct extras to write requests
 */
function serverSyncEffect(variable, requestExtras) {
    // Assumption: the set of extras is unique to the store, i.e. the variable will not be used under different sets of extras
    // Otherwise we sync multiple different stores but then we treat them as the same atom, which would cause issues
    STORE_EXTRAS_MAP.set(variable.uid, requestExtras);
    const eff = syncEffect({
        /** Use store uid as the unique identifier */
        itemKey: variable.uid,
        refine: mixed(),
        storeKey: 'ServerVariable',
    });
    return (params) => {
        // NOTE:
        // This is a hack to work around a "bug"/quirk I found in Recoil.
        // Turns out within the atom effect logic, the initial storage read only runs when params.storeID is found within the
        // Recoil internal atom registries - see https://github.com/facebookexperimental/Recoil/blob/c1b97f3a0117cad76cbc6ab3cb06d89a9ce717af/packages/recoil-sync/RecoilSync.js#L560,
        // if `registries.getStorage` returns null then the atom initial read is not ran.
        // However, when registering the atom and its effect within e.g. a useRecoilCallback, such as within
        // actions, the snapshot provided is cloned https://github.com/facebookexperimental/Recoil/blob/c1b97f3a0117cad76cbc6ab3cb06d89a9ce717af/packages/recoil/hooks/Recoil_useRecoilCallback.js#L88
        // and provided a new storeID https://github.com/facebookexperimental/Recoil/blob/c1b97f3a0117cad76cbc6ab3cb06d89a9ce717af/packages/recoil/core/Recoil_Snapshot.js#L85 .
        // This might be intentional on their side:
        // > If an atom is used first for a snapshot then it will execute the effect to initialize for that local snapshot, which would not affect the rendered state. Updating snapshots should not update the rendered state.
        // (as per https://github.com/facebookexperimental/Recoil/issues/1407#issuecomment-966666105)
        // This however breaks our assumptions that the effect always runs.
        // Fortunately, the params have the original storeID as the parentStoreID_UNSTABLE, so we run the effect here
        // with storeID set to the original parentStoreID_UNSTABLE which makes the `storage.read()` run.
        const updatedParams = params.parentStoreID_UNSTABLE ? Object.assign(Object.assign({}, params), { storeID: params.parentStoreID_UNSTABLE }) : params;
        // debugger;
        const cleanup = eff(updatedParams);
        return () => {
            cleanup === null || cleanup === void 0 ? void 0 : cleanup();
        };
    };
}
const STATE_SYNCHRONIZER = new StateSynchronizer();
/**
 * Get or register a server variable from the atom registry
 *
 * @param variable variable to register
 * @param extras request extras to be merged into the options
 */
export function getOrRegisterServerVariable(variable, extras) {
    if (!atomFamilyRegistry.has(variable.uid)) {
        atomFamilyRegistry.set(variable.uid, atomFamily({
            // NOTE: No default provided, atom starts in a pending state
            effects: (extrasSerializable) => {
                // Effect to synchronize changes across atoms of the same family
                const familySync = ({ onSet, setSelf, resetSelf, node }) => {
                    // Register the atom in the state synchronizer if not already registered
                    if (!STATE_SYNCHRONIZER.isRegistered(variable.uid)) {
                        STATE_SYNCHRONIZER.register(variable.uid, null);
                    }
                    else {
                        const currentState = STATE_SYNCHRONIZER.getCurrentState(variable.uid);
                        if ((currentState === null || currentState === void 0 ? void 0 : currentState.type) !== 'initial') {
                            // Otherwise synchronize the initial value
                            setSelf(currentState === null || currentState === void 0 ? void 0 : currentState.value);
                        }
                    }
                    // Synchronize changes across atoms of the same family
                    const unsub = STATE_SYNCHRONIZER.subscribe(variable.uid, (update) => {
                        if (update.type === 'initial') {
                            return;
                        }
                        // skip updates from the same atom
                        if (update.nodeKey === node.key) {
                            return;
                        }
                        if (update.isReset) {
                            resetSelf();
                        }
                        else {
                            setSelf(update.value);
                        }
                    });
                    onSet((newValue, oldValue, isReset) => {
                        STATE_SYNCHRONIZER.notify(variable.uid, {
                            isReset,
                            nodeKey: node.key,
                            oldValue,
                            type: 'update',
                            value: newValue,
                        });
                    });
                    return unsub;
                };
                return [familySync, serverSyncEffect(variable, extrasSerializable)];
            },
            key: variable.uid,
        }));
    }
    const family = atomFamilyRegistry.get(variable.uid);
    const extrasSerializable = new RequestExtrasSerializable(extras);
    const atomInstance = family(extrasSerializable);
    // Register the atom instance in the atomFamilyMembersRegistry so we can retrieve it later
    if (!atomFamilyMembersRegistry.has(family)) {
        atomFamilyMembersRegistry.set(family, new Map());
    }
    atomFamilyMembersRegistry.get(family).set(extrasSerializable.toJSON(), atomInstance);
    return atomInstance;
}
/** Resolve a server variable to its resolved form  */
export function resolveServerVariable(variable, extras, resolver) {
    return __awaiter(this, void 0, void 0, function* () {
        const atom = getOrRegisterServerVariable(variable, extras);
        // the value stored on the client is the sequence number
        const seqNumber = yield resolver(atom);
        return {
            type: 'server',
            uid: variable.uid,
            sequence_number: seqNumber,
        };
    });
}
export function resolveServerVariableStatic(variable, snapshot) {
    let seqNumber = 0;
    // use the atom if already has a value in the snapshot, otherwise assume 0
    const family = atomFamilyRegistry.get(variable.uid);
    if (family) {
        const extrasSerializable = new RequestExtrasSerializable({});
        const atomInstance = family(extrasSerializable);
        const atomValue = snapshot.getLoadable(atomInstance).valueMaybe();
        if (typeof atomValue === 'number') {
            seqNumber = atomValue;
        }
    }
    return {
        type: 'server',
        uid: variable.uid,
        sequence_number: seqNumber,
    };
}
/**
 * RecoilSync implementation for ServerVariable
 *
 * - read: GET from /api/core/store/:store_uid
 * - listen: subscribed to `serverVariableMessages$` on WsClient
 */
export function ServerVariableSyncProvider({ children }) {
    const { client } = React.useContext(WebSocketCtx);
    const getStoreValue = React.useCallback((itemKey) => __awaiter(this, void 0, void 0, function* () {
        var _a;
        const serializableExtras = STORE_EXTRAS_MAP.get(itemKey);
        const response = yield request(`/api/core/server-variable/${itemKey}/sequence`, (_a = serializableExtras === null || serializableExtras === void 0 ? void 0 : serializableExtras.extras) !== null && _a !== void 0 ? _a : {});
        yield handleAuthErrors(response, { authenticationFailureRedirect: 'login' });
        yield validateResponse(response, `Failed to fetch the sequence number for key: ${itemKey}`);
        const { sequence_number } = yield response.json();
        return sequence_number;
    }), []);
    const listenToStoreChanges = React.useCallback(({ updateItem }) => {
        if (!client) {
            return;
        }
        // Subscribe to regular value updates
        const valueSub = client.serverVariableMessages$().subscribe((message) => {
            updateItem(message.uid, message.sequence_number);
        });
        return () => {
            valueSub.unsubscribe();
        };
    }, [client]);
    return (_jsx(RecoilSync, { listen: listenToStoreChanges, read: getStoreValue, storeKey: "ServerVariable", children: children }));
}
//# sourceMappingURL=server-variable.js.map