"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SampledAnalytics = exports.ThrottledAnalytics = exports.ToggleableAnalytics = exports.NoopAnalytics = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
class Queue {
    constructor(applyFn) {
        this.queue = [];
        this.state = 'paused';
        this.applyFn = applyFn;
    }
    push(val) {
        switch (this.state) {
            case 'paused':
                this.queue.push(val);
                return;
            case 'enabled':
                this.applyFn(val);
                return;
            case 'disabled':
            default:
                return;
        }
    }
    enable() {
        this.state = 'enabled';
        const queue = this.queue;
        this.queue = [];
        queue.forEach((val) => {
            this.applyFn(val);
        });
    }
    disable() {
        this.state = 'disabled';
        this.queue = [];
    }
    pause() {
        this.state = 'paused';
    }
    getState() {
        return this.state;
    }
}
class NoopAnalytics {
    identify() { }
    track() { }
    flush() {
        return Promise.resolve();
    }
}
exports.NoopAnalytics = NoopAnalytics;
function addTimestamp(message) {
    const timestampDate = message.timestamp instanceof Date || message.timestamp === undefined
        ? message.timestamp
        : new Date(message.timestamp);
    return { ...message, timestamp: timestampDate };
}
class ToggleableAnalytics {
    constructor(target = new NoopAnalytics()) {
        this._queue = new Queue((item) => {
            if (item[0] === 'identify') {
                this._target.identify(...item[1]);
            }
            if (item[0] === 'track') {
                this._target.track(...item[1]);
            }
        });
        this._target = target;
    }
    identify(...args) {
        this._validateArgs(args);
        this._queue.push(['identify', [addTimestamp(args[0])]]);
    }
    track(...args) {
        this._validateArgs(args);
        this._queue.push(['track', [addTimestamp(args[0])]]);
    }
    enable() {
        if (this._pendingError) {
            throw this._pendingError;
        }
        this._queue.enable();
    }
    disable() {
        this._pendingError = undefined;
        this._queue.disable();
    }
    pause() {
        this._queue.pause();
    }
    _validateArgs([firstArg]) {
        if (!('userId' in firstArg && firstArg.userId) &&
            !('anonymousId' in firstArg && firstArg.anonymousId)) {
            const err = new Error('Telemetry setup is missing userId or anonymousId');
            switch (this._queue.getState()) {
                case 'enabled':
                    throw err;
                case 'paused':
                    this._pendingError ??= err;
                    break;
                default:
                    break;
            }
        }
    }
    async flush() {
        return await this._target.flush();
    }
}
exports.ToggleableAnalytics = ToggleableAnalytics;
async function lockfile(filepath, staleDuration = 43_200_000) {
    let intervalId;
    const lockfilePath = `${filepath}.lock`;
    const unlock = async () => {
        clearInterval(intervalId);
        try {
            return await fs_1.default.promises.rmdir(lockfilePath);
        }
        catch {
        }
    };
    try {
        await fs_1.default.promises.mkdir(lockfilePath);
        intervalId = setInterval(() => {
            const now = Date.now();
            fs_1.default.promises.utimes(lockfilePath, now, now).catch(() => { });
        }, staleDuration / 2);
        intervalId.unref?.();
        return unlock;
    }
    catch (e) {
        if (e.code !== 'EEXIST') {
            throw e;
        }
        const stats = await fs_1.default.promises.stat(lockfilePath);
        if (Date.now() - stats.mtimeMs > staleDuration) {
            await fs_1.default.promises.rmdir(lockfilePath);
            return lockfile(filepath, staleDuration);
        }
        throw new Error(`File ${filepath} already locked`);
    }
}
class ThrottledAnalytics {
    constructor({ target, throttle } = {}) {
        this.trackQueue = new Queue((message) => {
            if (this.shouldEmitAnalyticsEvent()) {
                this.target.track(message);
                this.throttleState.count++;
            }
        });
        this.target = new NoopAnalytics();
        this.currentUserId = null;
        this.throttleOptions = null;
        this.throttleState = { count: 0, timestamp: Date.now() };
        this.restorePromise = Promise.resolve();
        this.unlock = () => Promise.resolve();
        this.target = target ?? this.target;
        this.throttleOptions = throttle ?? this.throttleOptions;
    }
    get metadataPath() {
        if (!this.throttleOptions) {
            throw new Error('Metadata path is not avaialble if throttling is disabled');
        }
        if (!this.currentUserId) {
            throw new Error('Metadata path is not avaialble if userId is not set');
        }
        const { throttleOptions: { metadataPath }, currentUserId: userId, } = this;
        return path_1.default.resolve(metadataPath, `am-${userId}.json`);
    }
    identify(message) {
        message = addTimestamp(message);
        if (this.currentUserId) {
            throw new Error('Identify can only be called once per user session');
        }
        this.currentUserId = message.userId ?? message.anonymousId;
        this.restorePromise = this.restoreThrottleState().then((enabled) => {
            if (!enabled) {
                this.trackQueue.disable();
                return;
            }
            if (this.shouldEmitAnalyticsEvent()) {
                this.target.identify(message);
                this.throttleState.count++;
            }
            this.trackQueue.enable();
        });
    }
    track(message) {
        this.trackQueue.push(addTimestamp(message));
    }
    async restoreThrottleState() {
        if (!this.throttleOptions) {
            return true;
        }
        if (!this.currentUserId) {
            throw new Error('Trying to restore throttle state before userId is set');
        }
        try {
            this.unlock = await lockfile(this.metadataPath, this.throttleOptions.lockfileStaleDuration);
        }
        catch (e) {
            return false;
        }
        try {
            this.throttleState = JSON.parse(await fs_1.default.promises.readFile(this.metadataPath, 'utf8'));
        }
        catch (e) {
            if (e.code !== 'ENOENT') {
                return false;
            }
        }
        return true;
    }
    shouldEmitAnalyticsEvent() {
        if (!this.throttleOptions) {
            return true;
        }
        if (Date.now() - this.throttleState.timestamp >
            (this.throttleOptions.timeframe ?? 60_000)) {
            this.throttleState.timestamp = Date.now();
            this.throttleState.count = 0;
            return true;
        }
        return this.throttleState.count < this.throttleOptions.rate;
    }
    async flush() {
        if (!this.throttleOptions) {
            await this.target.flush();
            return;
        }
        if (!this.currentUserId) {
            throw new Error('Trying to persist throttle state before userId is set');
        }
        try {
            await this.restorePromise;
        }
        catch {
        }
        await fs_1.default.promises.writeFile(this.metadataPath, JSON.stringify(this.throttleState));
        await this.unlock();
        await this.target.flush();
    }
}
exports.ThrottledAnalytics = ThrottledAnalytics;
class SampledAnalytics {
    constructor(configuration) {
        this.isEnabled = configuration.sampling();
        this.target = configuration.target || new NoopAnalytics();
    }
    get enabled() {
        return this.isEnabled;
    }
    identify(message) {
        this.isEnabled && this.target.identify(message);
    }
    track(message) {
        this.isEnabled && this.target.track(message);
    }
    async flush() {
        return await this.target.flush();
    }
}
exports.SampledAnalytics = SampledAnalytics;
//# sourceMappingURL=analytics-helpers.js.map