"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActorRunnerBuilder = exports.ActorRunner = exports.DEFAULT_LOCAL_APP_ID = exports.DEFAULT_CAPACITY = void 0;
const infra_1 = require("./infra");
const bsondeser_1 = require("./infra/bsondeser");
const utils_1 = require("@darlean/utils");
const utils_2 = require("@darlean/utils");
const instances_1 = require("./instances");
const remoteinvocation_1 = require("./remoteinvocation");
const transportremote_1 = require("./transportremote");
const base_1 = require("@darlean/base");
const actor_lock_suite_1 = require("@darlean/actor-lock-suite");
const actor_lock_suite_2 = __importDefault(require("@darlean/actor-lock-suite"));
const distributedactorlock_1 = require("./distributedactorlock");
const inprocesstransport_1 = require("./infra/inprocesstransport");
const distributedactorregistry_1 = require("./distributedactorregistry");
const actor_registry_suite_1 = require("@darlean/actor-registry-suite");
const actor_registry_suite_2 = __importDefault(require("@darlean/actor-registry-suite"));
const distributedpersistence_1 = require("./distributedpersistence");
const persistence_suite_1 = __importDefault(require("@darlean/persistence-suite"));
const fs_persistence_suite_1 = __importDefault(require("@darlean/fs-persistence-suite"));
const shared_1 = require("./shared");
exports.DEFAULT_CAPACITY = 1000;
exports.DEFAULT_LOCAL_APP_ID = 'local';
/**
 * Class that hosts actors and (depending on the configuration) exposes them remotely. Typically
 * created by means of an {@link ActorRunnerBuilder}.
 */
class ActorRunner {
    constructor(portal) {
        this.delayedStop = 0;
        this.stopped = false;
        this.stopping = false;
        this.starting = false;
        this.portal = portal;
        this.waiters = [];
        this.starters = [];
        this.stoppers = [];
    }
    getPortal() {
        return this.portal;
    }
    async start() {
        if (this.starting) {
            return;
        }
        this.starting = true;
        for (const starter of this.starters.sort((a, b) => a.order - b.order)) {
            (0, utils_1.notifier)().debug('io.darlean.runner.starter.Invoke', 'Invoking starter [Name].', () => ({ Name: starter.name }));
            try {
                await starter.starter();
                (0, utils_1.notifier)().debug('io.darlean.runner.starter.Done', 'Starter [Name] done', () => ({ Name: starter.name }));
            }
            catch (e) {
                (0, utils_1.notifier)().error('io.darlean.runner.starter.Failed', 'Unable to invoke starter [Name]: [Error]', () => ({
                    Name: starter.name,
                    Error: e
                }));
                throw e;
            }
        }
    }
    async stop() {
        if (this.stopped) {
            return;
        }
        if (this.stopping) {
            await this.run();
            return;
        }
        this.stopping = true;
        for (const stopper of this.stoppers.sort((a, b) => b.order - a.order)) {
            await stopper.stopper();
        }
        this.stopped = true;
        const waiters = this.waiters;
        this.waiters = [];
        for (const waiter of waiters) {
            waiter();
        }
    }
    addStarter(starter, order, name) {
        this.starters.push({ order, starter, name });
    }
    addStopper(stopper, order, name) {
        this.stoppers.push({ order, stopper, name });
    }
    /**
     * Starts the actor runner when not already started before, and waits until the actor runner is stopped, either explicitly
     * by calling the {@link stop} method, or because of application termination signals.
     */
    async run() {
        if (this.stopped) {
            return;
        }
        // Will do nothing when already starting or started
        await this.start();
        return new Promise((resolve) => {
            this.waiters.push(resolve);
        });
    }
}
exports.ActorRunner = ActorRunner;
/**
 * Class that can be used to build an {@link ActorRunner} based on provided configuration.
 */
class ActorRunnerBuilder {
    constructor() {
        this.transportMechanism = '';
        this.appId = exports.DEFAULT_LOCAL_APP_ID;
        this.actors = [];
    }
    /**
     * Registers an individual actor that must be hosted by the actor runner.
     * @param options The options for the actor.
     * @returns The builder
     */
    registerActor(options) {
        this.actors.push(options);
        return this;
    }
    /**
     * Registers a suite of actors that must be hosted by the actor runner.
     * @param suite The suite to be registered
     * @returns The builder
     */
    registerSuite(suite) {
        for (const options of suite.getRegistrationOptions()) {
            this.registerActor(options);
        }
        return this;
    }
    /**
     * Sets a list of appId's on which the runner will search for actors that do not
     * explicitly have their apps setting configured.
     * @param hosts
     */
    setDefaultApps(apps) {
        this.defaultApps = apps;
        return this;
    }
    setRuntimeApps(apps) {
        this.runtimeApps = apps;
        this.registerActor({
            type: actor_registry_suite_1.ACTOR_REGISTRY_SERVICE,
            kind: 'singular',
            apps: apps
        });
        return this;
    }
    /**
     * Configures the builder to allow remote access via Nats.
     * @param appId The app-id under which the actor runner will register to the cluster.
     * @param transport Optional transport that is used for remote access. When omitted,
     * the builder creates a {@link NatsTransport}.
     */
    setRemoteAccess(appId, transport) {
        this.appId = appId;
        this.transport = transport;
        this.transportMechanism = 'nats';
        return this;
    }
    hostActorLock(nodes, redundancy) {
        const suite = (0, actor_lock_suite_2.default)({
            locks: nodes,
            id: [],
            redundancy
        });
        this.registerSuite(suite);
    }
    hostActorRegistry(nodes) {
        const suite = (0, actor_registry_suite_2.default)(nodes);
        this.registerSuite(suite);
    }
    hostPersistence(options) {
        const suite = (0, persistence_suite_1.default)(options);
        this.registerSuite(suite);
    }
    hostFsPersistence(options) {
        const suite = (0, fs_persistence_suite_1.default)(options);
        this.registerSuite(suite);
    }
    /**
     * Overrides the default distributed persistence which is automatically enabled when {@link setPersistence} is not invoked.
     * @param persistence
     */
    setPersistence(factory) {
        this.persistenceFactory = factory;
    }
    /**
     * Builds a new {@link ActorRunner} based on the provided configuration.
     * @returns a new ActorRunner
     */
    build() {
        const time = this.createTime();
        this.time = time;
        const backoff = this.createBackOff(time);
        const multiContainer = this.createMultiContainer();
        this.multiContainer = multiContainer;
        const portal = this.createPortal(backoff, this.transport, time, multiContainer);
        this.portal = portal;
        if (!this.persistenceFactory) {
            this.persistenceFactory = this.createPersistence(portal, new bsondeser_1.BsonDeSer());
        }
        const ar = new ActorRunner(portal);
        this.configurePortal(ar);
        this.registerAutoStarts(ar);
        return ar;
    }
    /**
     *
     * @returns a reference to the persistence service of the actor runner.
     */
    getPersistence() {
        return typeof this.persistenceFactory === 'function' ? this.persistenceFactory('') : this.persistenceFactory;
    }
    createTime() {
        return new utils_2.Time();
    }
    createBackOff(time) {
        return new remoteinvocation_1.ExponentialBackOff(time, 10, 4);
    }
    createActorLock(portal, time, appId) {
        const servicePortal = portal.typed(actor_lock_suite_1.ACTOR_LOCK_SERVICE);
        const service = servicePortal.retrieve([]);
        return new distributedactorlock_1.DistributedActorLock(time, service, appId);
    }
    createPortal(backoff, transport, time, multiContainer) {
        return this.createRemotePortal(backoff, this.appId, transport, time, multiContainer, this.transportMechanism);
    }
    configurePortal(ar) {
        const handler = (signal, code, error) => {
            if (signal) {
                console.log('Received stop signal', signal);
            }
            else if (code) {
                console.log('Received exit with code', code);
            }
            else if (error) {
                console.log('Uncaught error', error);
            }
            setImmediate(async () => {
                try {
                    await ar.stop();
                }
                catch (e) {
                    console.log(e);
                }
                console.log('STOPPED');
            });
        };
        ar.addStarter(async () => {
            (0, utils_1.onApplicationStop)(handler);
            if (this.remote) {
                (0, utils_1.notifier)().info('io.darlean.transport.Init', 'Initializing transport...');
                try {
                    await this.remote.init();
                    (0, utils_1.notifier)().info('io.darlean.transport.Initialized', 'Transport initialized.');
                }
                catch (e) {
                    (0, utils_1.notifier)().error('io.darlean.transport.Failed', 'Failed to initialize transport: [Error]', () => ({
                        Error: e
                    }));
                    throw e;
                }
            }
            if (this.distributedRegistry) {
                this.distributedRegistry.start();
            }
        }, 50, 'default');
        ar.addStopper(async () => {
            (0, utils_1.offApplicationStop)(handler);
            if (this.distributedRegistry) {
                await this.distributedRegistry.stop();
            }
            await this.multiContainer?.finalize();
            if (this.remote) {
                await this.remote.finalize();
            }
        }, 50, 'default');
    }
    createMultiContainer() {
        return new instances_1.MultiTypeInstanceContainer();
    }
    fillContainers(multiContainer, portal, actorLock) {
        for (const actor of this.actors) {
            const creator = actor.creator;
            if (creator) {
                const container = actor.container ??
                    new instances_1.InstanceContainer(actor.type, (id) => {
                        const timers = [];
                        const context = this.createActorCreateContext(actor.type, id, timers);
                        return {
                            instance: creator(context),
                            afterCreate: (wrapper) => {
                                for (const timer of timers) {
                                    timer.setWrapper(wrapper);
                                }
                            }
                        };
                    }, actor.capacity ?? exports.DEFAULT_CAPACITY, actor.kind === 'singular' ? actorLock : undefined);
                multiContainer.register(actor.type, container);
            }
        }
    }
    createRemotePortal(backoff, appId, transport, time, multiContainer, mechanism) {
        const remote = this.createRemote(appId, multiContainer, mechanism, transport);
        this.remote = remote;
        const registry = new remoteinvocation_1.ActorRegistry();
        const placementCache = new remoteinvocation_1.PlacementCache(10000);
        const portal = new remoteinvocation_1.RemotePortal(remote, backoff, registry, placementCache);
        const registryService = portal.retrieve(actor_registry_suite_1.ACTOR_REGISTRY_SERVICE, []);
        const distributedRegistry = new distributedactorregistry_1.DistributedActorRegistry(registryService, time, appId, registry);
        this.distributedRegistry = distributedRegistry;
        portal.setRegistry(distributedRegistry);
        const actorLock = this.createActorLock(portal, time, appId);
        this.fillContainers(multiContainer, portal, actorLock);
        for (const actor of this.actors) {
            const hosts = actor.apps ?? this.defaultApps ?? [this.appId];
            if (hosts) {
                for (const host of hosts) {
                    const placement = actor.placement
                        ? { ...actor.placement }
                        : {
                            version: new Date().toISOString()
                        };
                    if (actor.kind === 'singular' && placement.sticky === undefined) {
                        placement.sticky = true;
                    }
                    registry.addMapping(actor.type, host, placement);
                }
            }
        }
        return portal;
    }
    createRemote(appId, container, mechanism, transport) {
        if (!transport) {
            const deser = new bsondeser_1.BsonDeSer();
            if (mechanism === 'nats') {
                transport = new infra_1.NatsTransport(deser);
            }
            else {
                transport = new inprocesstransport_1.InProcessTransport(deser);
            }
        }
        this.transport = transport;
        return new transportremote_1.TransportRemote(appId, transport, container);
    }
    createPersistence(portal, deser) {
        return (_type, specifiers) => {
            const servicePortal = portal.typed(base_1.PERSISTENCE_SERVICE);
            const service = servicePortal.retrieve([]);
            return new distributedpersistence_1.DistributedPersistence(service, deser, specifiers);
        };
    }
    createActorCreateContext(type, id, timers) {
        type = (0, shared_1.normalizeActorType)(type);
        if (!this.portal) {
            throw new Error('No portal assigned');
        }
        const persistenceFactory = this.persistenceFactory;
        if (!persistenceFactory) {
            throw new Error('No persistence assigned');
        }
        const time = this.time;
        if (time === undefined) {
            throw new Error('No time assigned');
        }
        return {
            id,
            portal: this.portal,
            persistence: (specifiers) => {
                // The id-length is there to prevent malicious code from accessing persistent data
                // from other actors.
                // When actor 1 has id ['a', 'b'] and stores state in ['c];
                // actor 2 with id ['a'] and state in ['b', 'c'] would mess with actor 1's data.
                // Including id length prevents this: ['type', '2', 'a', 'b', 'c'] !== ['type', '1', 'a', 'b', 'c'].
                if (typeof specifiers === 'string') {
                    specifiers = [specifiers];
                }
                const typePersistence = typeof persistenceFactory === 'function' ? persistenceFactory(type, specifiers) : persistenceFactory;
                return typePersistence.sub([type, id.length.toString(), ...id]);
            },
            time,
            newVolatileTimer: () => {
                const timer = new instances_1.VolatileTimer(time);
                timers.push(timer);
                return timer;
            }
        };
    }
    registerAutoStarts(ar) {
        const autoStartActors = this.actors.filter((a) => a.startActions);
        if (autoStartActors.length > 0) {
            ar.addStarter(async () => {
                for (const actor of autoStartActors) {
                    const portal = this.portal?.typed(actor.type);
                    if (portal && actor.startActions) {
                        for (const a of actor.startActions) {
                            (0, utils_1.notifier)().info('io.darlean.runner.AutoStarting', `Starting [Name]...`, () => ({ Name: a.name }));
                            // eslint-disable-next-line @typescript-eslint/no-explicit-any
                            await portal.retrieve(a.id)[a.action](...(a.arguments ?? []));
                            (0, utils_1.notifier)().info('io.darlean.runner.AutoStarted', `Started [Name]`, () => ({ Name: a.name }));
                        }
                    }
                }
            }, 99, 'Autostart Actors');
        }
    }
}
exports.ActorRunnerBuilder = ActorRunnerBuilder;
