var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { EventEmitter } from 'eventemitter3';
import Queue from 'p-queue';
import { deepClone, deepFreeze, LogClass } from './utils.js';
const _snapshot = Symbol();
/**
 * The core engine for running applications that interface with blockchain-driven architectures.
 * Provides mechanisms for reading from and writing to the blockchain, as well as invoking app-specific methods.
 *
 * @category Machine
 */
let Machine = class Machine extends EventEmitter {
    /**
     * @param appClass - The class that represents the application's logic and state.
     * @param driver - The reader driver to read blockchain events.
     * @param writer - The writer to persist data to the blockchain.
     */
    constructor(appClass, reader, writer, recorder, options) {
        super();
        this.queue = new (Queue.default ? Queue.default : Queue)({ concurrency: 1 }); // ESM <> CommonJS hack
        this.instance = new appClass();
        this.reader = reader;
        this.writer = writer;
        this.recorder = recorder;
        this.options = options || { emitAppEvents: false };
        this[_snapshot] = {
            action: undefined,
            state: this.instance.state, // ! This is a reference! Not a clone! Always deepClone() when saving this somewhere else
        };
        if (this.recorder) {
            this.recorder.afterDispatch(deepFreeze(deepClone(this[_snapshot])));
        }
        this.reader.initialize(this.dispatchAction.bind(this));
    }
    on(event, listener) {
        return super.on(event, listener);
    }
    /**
     * Instructs the reader to begin dispatching events.
     * Calls the reader's start function with the latest action id if present
     *
     * @param snapshot - Resume from a snapshot. Informs the reader of the latest snapshot so that the reader can skip any actions prior to this.
     */
    start() {
        var _a;
        this.reader.start((_a = this[_snapshot].action) === null || _a === void 0 ? void 0 : _a.id);
    }
    /**
     * Instructs the reader to stop dispatching actions.
     */
    stop() {
        this.reader.stop();
    }
    /**
     * Instructs the reader to fetch the latest events.
     * This is different from start(), which opens a realtime websocket that drives your App continuously.
     * Do not mix the usage of start() and fetch()
     */
    async fetch() {
        var _a;
        return this.reader.fetch((_a = this[_snapshot].action) === null || _a === void 0 ? void 0 : _a.id);
    }
    /**
     * Creates a snapshot.
     * This function will create a snapshot after an action has been fully completed.
     */
    async createSnapshot() {
        if (!this.recorder) {
            throw new Error('No Recorder available. Provide a recorder to Machine via the constructor');
        }
        if (!this.recorder.createSnapshot || typeof this.recorder.createSnapshot !== 'function') {
            throw new Error('No Recorder with createSnapshot() available. Implement createSnapshot() on your Recorder');
        }
        return this.queue.add(async () => {
            const snapshot = deepClone(this[_snapshot]);
            await this.recorder.createSnapshot(snapshot);
            return snapshot;
        }, {
            throwOnTimeout: true,
        });
    }
    /**
     * Loads a snapshot into the state of the App instance.
     * After calling this function, getInstance().state will equal that of the loaded snapshot
     * @param id - The Action.id of the snapshot to load
     */
    async loadSnapshot(id) {
        var _a;
        if (!((_a = this.recorder) === null || _a === void 0 ? void 0 : _a.loadSnapshot) || typeof this.recorder.loadSnapshot !== 'function') {
            throw new Error('No Recorder with loadSnapshot() available. Give a Recorder instance to AppEngine via the constructor and implement loadSnapshot().');
        }
        const snapshot = await this.recorder.loadSnapshot(id);
        if (!snapshot) {
            // console.error('The Recorder did not provide a snapshot. Doing nothing.');
            return;
        }
        return this.queue.add(() => {
            this.instance.state = deepClone(snapshot.state);
            this[_snapshot] = {
                action: snapshot.action,
                ctx: snapshot.ctx,
                state: this.instance.state,
            };
        });
    }
    /**
     * Gets a proxied version of the app instance where methods are wrapped
     * to automatically write actions to the action storage.
     *
     * Access to instance.state is readonly via a cloned copy
     *
     * @returns The wrapped application instance.
     */
    getInstance() {
        return new Proxy(this.instance, {
            get: (target, prop) => {
                if (typeof prop === 'symbol' || !(prop in target)) {
                    return undefined;
                }
                if (prop === 'state') {
                    // Return a cloned copy of the state, freeze is to discourage modification
                    return deepFreeze(deepClone(target.state));
                }
                if (typeof target[prop] !== 'function' || prop.startsWith('_')) {
                    return Reflect.get(target, prop);
                }
                // If it's a function, return a new function that calls the writer
                return async (...args) => {
                    const action = {
                        // id: '?', // TODO: How to deal with action id's that we don't know yet? Does it even matter?
                        f: prop,
                        args,
                    };
                    await this.writer.write(action);
                };
            },
            set(target, prop, value) {
                if (prop === 'state') {
                    throw new Error('Direct modification of state is forbidden');
                }
                return Reflect.set(target, prop, value);
            },
        });
    }
    /**
     * Gets a proxied version of the app instance where methods are wrapped
     * to automatically write events to the blockchain when invoked.
     *
     * @returns The wrapped application instance.
     */
    useMiddleware(methodName, middlewareFn) {
        const originalMethod = this.instance[methodName];
        if (typeof originalMethod !== 'function') {
            console.warn(`No method named ${String(methodName)} found on the instance.`);
            return;
        }
        this.instance[methodName] = async (ctx, ...args) => {
            const next = (newArgs) => originalMethod.apply(this.instance, [ctx, ...newArgs]);
            return middlewareFn(ctx, args, next);
        };
    }
    /**
     * Dispatches an action to the appropriate method on the app instance.
     * Passed to a reader via the Reader.initialize function.
     * Called by a reader to dispatch actions.
     *
     * @param action - The action data.
     * @param additionalContext - Additional context data that will be accessible from ctx.reader inside methods.
     */
    dispatchAction(action, additionalContext, isResynchronizing) {
        return this.queue
            .add(async () => {
            const ctx = deepFreeze({
                action,
                isResynchronizing: isResynchronizing,
                reader: additionalContext,
            });
            // Check if the method starts with an underscore (making it private).
            if (action.f.startsWith('_')) {
                throw new Error(`Cannot dispatch to private method ${action.f}. Methods starting with an underscore are considered private.`);
            }
            if (typeof this.instance[action.f] === 'function') {
                await this.instance[action.f](ctx, ...(action.args || []));
            }
            else {
                console.warn(`No method named ${action.f} found on the instance.`);
            }
            // Set this snapshot
            this[_snapshot] = {
                action: action,
                ctx: ctx,
                state: this.instance.state,
            };
            if (this.options.emitAppEvents) {
                const snapshot = deepFreeze(deepClone(this[_snapshot]));
                this.emit(action.f, snapshot);
            }
            if (this.recorder) {
                // If recorder instance is provided, record the snapshot.
                const snapshot = deepFreeze(deepClone(this[_snapshot]));
                this.recorder.afterDispatch(snapshot);
                this.emit('_snapshotRecorded', snapshot);
            }
        })
            .catch((error) => {
            console.error('Error executing method:', error);
        })
            .finally(() => {
            //console.log(JSON.stringify(this.instance, null, 2));
        });
    }
};
Machine = __decorate([
    LogClass()
], Machine);
export { Machine };
//# sourceMappingURL=Machine.js.map