"use strict";
/**
 * User code can invoke remote actors by first obtaining an {@link IPortal} instance (like {@link RemotePortal} or
 * {@link localinvocation.LocalPortal}) and then obtaining a local actor proxy by means of {@link IPortal.retrieve}.
 *
 * The default {@link IPortal} implementation, {@link RemotePortal}, has a reference to an {@link IRemote} to
 * perform the actual remote action calls (via {@link IRemote.invoke}). It converts the method calls on the
 * proxy objects to an {@link IInvokeOptions} object with an {@link IActorCallRequest} as content, then
 * performs the call via {@link IRemote.invoke}, awaits the answer (which is an {@link IInvokeResult} with an
 * {@link IActorCallResponse} as content), and returns the {@link IActorCallResponse.result} or throws an
 * {@link ApplicationError} (when the remote actor threw an exception) or a {@link FrameworkError} when the request
 * could not be complete due to technical causes.
 *
 * ## Backoff mechanism
 *
 * In order to handle short-living network issues and actor reincarnation without having to bother the user code
 * with that, the {@link RemotePortal} repeatedly retries to invoke the remote actor with certain time
 * intervals. This backoff mechanism is provided by an {@link IBackOff} instance like {@link ExponentialBackOff},
 * which provides a backoff with exponentially increasing delays.
 *
 * ## Actor placement
 *
 * In order to know to which destination (app) an {@link IActorCallRequest} should be sent to, the {@link RemotePortal}
 * uses the {@link IActorPlacement} information it receives via the {@link RemotePortal.addMapping} and {@link RemotePortal.removeMapping} methods.
 *
 * ## Local (in process) use
 *
 * For local use (that is, when all code and actors live in the same process), see module {@link localinvocation}.
 *
 * @packageDocumentation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrefixTypedPortal = exports.PrefixPortal = exports.RemotePortal = exports.PlacementCache = exports.ActorRegistry = exports.ImmediateBackOff = exports.ExponentialBackOff = exports.TypedPortal = void 0;
const base_1 = require("@darlean/base");
const utils_1 = require("@darlean/utils");
const utils_2 = require("@darlean/utils");
const instances_1 = require("./instances");
const shared_1 = require("./shared");
const transportremote_1 = require("./transportremote");
/**
 * Implementation of {@link ITypedPortal} that returns instances of a specific type
 * from a given portal.
 */
class TypedPortal {
    /**
     *
     * @param portal The portal from which instances are to be retrieved
     * @param type The type of actors of which instances are to be retrieved
     */
    constructor(portal, type) {
        this.portal = portal;
        this.type = type;
    }
    retrieve(id) {
        return this.portal.retrieve(this.type, id);
    }
    prefix(idPrefix) {
        return new PrefixTypedPortal(this, idPrefix);
    }
}
exports.TypedPortal = TypedPortal;
/**
 * Implementation of {@link IBackOff} that starts with an initial delay, which is
 * multiplied with a certain factor after every invocation. A spread of 50% is
 * applied to achieve (pseudo)random behaviour.
 */
class ExponentialBackOff {
    constructor(time, initial, factor) {
        this.time = time;
        this.factor = factor;
        this.initial = initial;
    }
    begin(maxDurationMS) {
        let value = this.initial;
        const maxTime = maxDurationMS === undefined ? undefined : this.time.machineTicks() + maxDurationMS;
        return async (aborter) => {
            const delay = Math.max(0, (Math.random() + 0.5) * value);
            value *= this.factor;
            const newTime = this.time.machineTicks() + delay;
            if (maxTime === undefined || newTime < maxTime) {
                await (0, utils_2.sleep)(delay, aborter);
                return true;
            }
            else {
                return false;
            }
        };
    }
}
exports.ExponentialBackOff = ExponentialBackOff;
/**
 * Implementation of {@link IBackOff} that returns immediately (that is, effectively
 * no backoff/delay).
 */
class ImmediateBackOff {
    begin(_maxDurationMS) {
        return async () => {
            return true;
        };
    }
}
exports.ImmediateBackOff = ImmediateBackOff;
class ActorRegistry {
    constructor() {
        this.mapping = new Map();
    }
    addMapping(actorType, receiver, placement) {
        actorType = (0, shared_1.normalizeActorType)(actorType);
        const entry = this.mapping.get(actorType);
        if (entry) {
            if (!entry.destinations.includes(receiver)) {
                entry.destinations.push(receiver);
            }
            if (placement && placement.version > (entry.placement?.version ?? '')) {
                entry.placement = placement;
            }
        }
        else {
            this.mapping.set(actorType, { destinations: [receiver], placement });
        }
    }
    removeMapping(actorType, receiver) {
        actorType = (0, shared_1.normalizeActorType)(actorType);
        const entry = this.mapping.get(actorType);
        if (entry) {
            const idx = entry.destinations.indexOf(receiver);
            if (idx >= 0) {
                entry.destinations.splice(idx, 1);
            }
        }
    }
    removeReceiver(receiver) {
        for (const actorType of this.mapping.keys()) {
            this.removeMapping(actorType, receiver);
        }
    }
    findPlacement(type) {
        return this.mapping.get((0, shared_1.normalizeActorType)(type));
    }
    getAll() {
        return this.mapping;
    }
}
exports.ActorRegistry = ActorRegistry;
class PlacementCache {
    constructor(capacity) {
        this.items = new Map();
        this.capacity = capacity;
    }
    get(actorType, id) {
        const key = (0, utils_1.encodeKey)([(0, shared_1.normalizeActorType)(actorType), ...id]);
        const value = this.items.get(key);
        if (value !== undefined) {
            // Move to end of LRU
            this.items.delete(key);
            this.items.set(key, value);
        }
        return value;
    }
    put(actorType, id, receiver) {
        const key = (0, utils_1.encodeKey)([(0, shared_1.normalizeActorType)(actorType), ...id]);
        this.items.delete(key);
        if (receiver !== undefined) {
            this.items.set(key, receiver);
            this.cleanup();
        }
    }
    cleanup() {
        if (this.items.size > this.capacity) {
            for (const key of this.items.keys()) {
                this.items.delete(key);
                if (this.items.size < this.capacity) {
                    break;
                }
            }
        }
    }
}
exports.PlacementCache = PlacementCache;
/**
 * Portal that provides access to actors in other apps via an {@link IRemote} instance.
 */
class RemotePortal {
    constructor(remote, backoff, registry, placementCache) {
        this.remote = remote;
        this.backoff = backoff;
        this.registry = registry;
        this.placementCache = placementCache;
    }
    setRegistry(value) {
        this.registry = value;
    }
    retrieve(type, id) {
        const instance = {};
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;
        let nextCallAborter;
        const p = Proxy.revocable(instance, {
            get: (_target, prop, _receiver) => {
                if (prop === 'aborter') {
                    return (value) => {
                        nextCallAborter = value;
                    };
                }
                // Assume the caller is only trying to get functions (not properties, fields etc)
                // we simply return a getter function implementation.
                return async function (...args) {
                    return await (0, utils_1.deeper)('io.darlean.remote.invoke', `${type}::${id}::${prop.toString()}`).perform(async () => {
                        let aborted = false;
                        const aborter = nextCallAborter;
                        nextCallAborter = undefined;
                        let subAborter;
                        // console.log('ABORTER', prop.toString(), aborter);
                        if (aborter) {
                            aborter.handle(() => {
                                aborted = true;
                                subAborter?.abort();
                            });
                        }
                        const actorType = (0, shared_1.normalizeActorType)(type);
                        const actionName = (0, shared_1.normalizeActionName)(prop.toString());
                        const content = {
                            actorType,
                            actorId: id,
                            actionName,
                            arguments: Array.from(args)
                        };
                        const info = { suggestion: undefined };
                        const placement = self.registry.findPlacement(type)?.placement;
                        if (placement?.sticky && self.placementCache) {
                            info.suggestion = self.placementCache.get(actorType, id);
                        }
                        const backoff = self.backoff.begin(5000);
                        const nested = [];
                        let breaking = false;
                        let idx = -1;
                        for (const destination of self.iterateDestinations(actorType, id, info)) {
                            if (breaking) {
                                break;
                            }
                            idx++;
                            let haveResult = false;
                            const result = await (0, utils_1.deeper)('io.darlean.remoteinvocation.try-one-destination', destination).perform(async () => {
                                // console.log('ITERATING', actorType, id, aborted, destination);
                                if (aborted) {
                                    throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_INVOKE_ERROR, 'Interrupted while invoking remote method [ActionName] on an instance of [ActorType]', {
                                        ActorType: actorType,
                                        ActionName: actionName
                                    }, undefined, nested);
                                }
                                const moment = Date.now();
                                if (destination !== '') {
                                    info.suggestion = undefined;
                                    subAborter = aborter ? new utils_1.Aborter() : undefined;
                                    const options = {
                                        destination,
                                        content,
                                        aborter: subAborter
                                    };
                                    const result = await self.remote.invoke(options);
                                    subAborter = undefined;
                                    if (result.errorCode) {
                                        nested.push(new base_1.FrameworkError(result.errorCode, result.errorParameters?.[transportremote_1.TRANSPORT_ERROR_PARAMETER_MESSAGE] ??
                                            result.errorCode, {
                                            requestTime: new Date(moment).toISOString(),
                                            requestOptions: options,
                                            requestResult: result
                                        }));
                                    }
                                    else {
                                        if (result.content) {
                                            let ok = false;
                                            try {
                                                const response = result.content;
                                                if (response.error) {
                                                    if (response.error.kind === 'framework') {
                                                        const redirect = response.error.parameters?.[base_1.FRAMEWORK_ERROR_PARAMETER_REDIRECT_DESTINATION];
                                                        if (redirect) {
                                                            info.suggestion = redirect[0];
                                                        }
                                                        nested.push((0, instances_1.toFrameworkError)(response.error));
                                                    }
                                                    else {
                                                        ok = true;
                                                        throw new base_1.ApplicationError(response.error.code, response.error.template, response.error.parameters, response.error.stack, response.error.nested, response.error.message);
                                                    }
                                                }
                                                else {
                                                    ok = true;
                                                    haveResult = true;
                                                    return response.result;
                                                }
                                            }
                                            finally {
                                                if (ok && placement?.sticky && self.placementCache) {
                                                    // console.log('UPDATE PLACEMENT', actorType, id, options.destination);
                                                    self.placementCache.put(actorType, id, options.destination);
                                                }
                                            }
                                        }
                                        else {
                                            throw new Error('No content');
                                        }
                                    }
                                }
                                else {
                                    nested.push(new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_NO_RECEIVERS_AVAILABLE, 'No receivers available at [RequestTime] to process an action on an instance of [ActorType]', {
                                        RequestTime: new Date(moment).toISOString(),
                                        ActorType: actorType,
                                        ActionName: actionName
                                    }));
                                }
                                // console.log('ITERATED, BACKING OFF');
                                (0, utils_1.currentScope)().debug('Aborted? [Aborted]', () => ({ Aborted: aborted }));
                                if (!aborted) {
                                    const skip = idx < 2 && info.suggestion;
                                    if (!skip) {
                                        subAborter = aborter ? new utils_1.Aborter() : undefined;
                                        try {
                                            (0, utils_1.currentScope)().debug('Backing off...');
                                            const backoffContinues = await (0, utils_1.deeper)('io.darlean.remoteinvocation.backoff').perform(() => backoff(aborter));
                                            (0, utils_1.currentScope)().debug('Backed off.');
                                            if (!backoffContinues) {
                                                breaking = true;
                                                return;
                                            }
                                        }
                                        finally {
                                            subAborter = undefined;
                                        }
                                    }
                                }
                            });
                            if (haveResult) {
                                return result;
                            }
                            // console.log('BACKED OFF');
                        }
                        // console.log('ALL RETRIES DONE');
                        if (placement?.sticky && self.placementCache) {
                            self.placementCache.put(actorType, id, undefined);
                        }
                        throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_INVOKE_ERROR, 'Failed to invoke remote method [ActionName] on an instance of [ActorType]: [FirstMessage] ... [LastMessage]', {
                            ActorType: actorType,
                            ActionName: actionName,
                            FirstMessage: nested[0]?.message ?? '',
                            LastMessage: nested[nested.length - 1]?.message ?? ''
                        }, undefined, nested);
                    });
                };
            }
        });
        return p.proxy;
    }
    typed(type) {
        return new TypedPortal(this, type);
    }
    prefix(idPrefix) {
        return new PrefixPortal(this, idPrefix);
    }
    findReceivers(actorType) {
        actorType = (0, shared_1.normalizeActorType)(actorType);
        return this.registry.findPlacement(actorType)?.destinations || [];
    }
    *iterateDestinations(type, id, info) {
        let randomReceiversDone = [];
        for (let i = 0; i < 10; i++) {
            (0, utils_1.currentScope)().debug('Iteration [I] with suggestion [Suggestion]', () => ({
                I: i,
                Suggestion: info.suggestion ?? 'no suggestion'
            }));
            if (info.suggestion) {
                const sug = info.suggestion;
                info.suggestion = undefined;
                yield sug;
            }
            else {
                // Derive the placement every loop iteration, as new apps may
                // have registered in between.
                const placement = this.registry.findPlacement(type)?.placement;
                let boundTo = '';
                if (placement?.bindIdx !== undefined) {
                    const idx = placement.bindIdx >= 0 ? placement.bindIdx : id.length + placement.bindIdx;
                    boundTo = id[idx];
                    if (boundTo) {
                        yield boundTo;
                        continue;
                    }
                }
                const receivers = this.findReceivers(type);
                if (receivers.length > 0) {
                    const idx = Math.floor(Math.random() * receivers.length);
                    const receiver = receivers[idx];
                    // When the receiver was already randomly selected before, first try
                    // to select one of the other available receivers. This to avoid that
                    // we randomly select the same receiver that is not available. For
                    // simplicity, we do this selection simply linear (not randomly).
                    if (randomReceiversDone.includes(receiver)) {
                        for (const receiver2 of receivers) {
                            if (!randomReceiversDone.includes(receiver2)) {
                                randomReceiversDone.push(receiver2);
                                yield receiver2;
                                break;
                            }
                        }
                        randomReceiversDone = [];
                        yield receivers[0];
                    }
                    else {
                        randomReceiversDone.push(receiver);
                        yield receiver;
                    }
                }
                else {
                    yield '';
                }
            }
        }
    }
}
exports.RemotePortal = RemotePortal;
class PrefixPortal {
    constructor(parent, idPrefix) {
        this.parent = parent;
        this.idPrefix = idPrefix;
    }
    retrieve(type, id) {
        return this.parent.retrieve(type, [...this.idPrefix, ...id]);
    }
    typed(type) {
        return new TypedPortal(this, type);
    }
    prefix(idPrefix) {
        return new PrefixPortal(this, idPrefix);
    }
}
exports.PrefixPortal = PrefixPortal;
class PrefixTypedPortal {
    constructor(parent, idPrefix) {
        this.parent = parent;
        this.idPrefix = idPrefix;
    }
    retrieve(id) {
        return this.parent.retrieve([...this.idPrefix, ...id]);
    }
    prefix(idPrefix) {
        return new PrefixTypedPortal(this, idPrefix);
    }
}
exports.PrefixTypedPortal = PrefixTypedPortal;
