"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.observableExpressionFunctionNames = exports.observableExpressionFunctions = void 0;
const tslib_1 = require("tslib");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const parseInt_1 = tslib_1.__importDefault(require("lodash/parseInt"));
const ExpressionEvaluationError_1 = require("../../parser/src/ExpressionEvaluationError");
const expressionFunctionUtils_1 = require("./expressionFunctionUtils");
const TypeExtensions_1 = require("../Extensions/TypeExtensions");
// numeric value(digits) followed by a single 's', 'm' or 'h' letter (case insensitive)
const TIMEFRAME_REGEX = /^(\d+)(s|m|h)$/i;
const SYSTEM_MAX_TIMEFRAME_SIZE = 86400000; // 24h
exports.observableExpressionFunctions = {
    WHERE: {
        handler(args, context) {
            return (0, expressionFunctionUtils_1.handleWhereFunction)(args, context);
        },
        isHiddenFromMenu: true,
        description: 'Limits the rows against which the Observable expression will be evaluated',
        signatures: ['<main_query> WHERE <boolean_query>'],
        examples: ['<main_query> WHERE <boolean_query>', '<main_query> WHERE QUERY("abc")'],
        hasEagerEvaluation: true,
        category: 'conditional',
    },
    ROW_CHANGE: {
        handler(args, context) {
            const operandParameter = (0, expressionFunctionUtils_1.extractParameter)('ROW_CHANGE', 'operand', ['COUNT', 'MAX', 'MIN', 'NONE'], args);
            const timeframeParameter = (0, expressionFunctionUtils_1.extractParameter)('ROW_CHANGE', 'config', ['TIMEFRAME'], args);
            let dataChangeLog$ = (0, expressionFunctionUtils_1.getDataChangeLog$)(context, operandParameter.column);
            switch (operandParameter.name) {
                case 'COUNT': {
                    const timeframeChange$ = getTrailingRowCountChange$(dataChangeLog$, timeframeParameter.value, operandParameter.value);
                    return getDataChangeCount$(dataChangeLog$, timeframeChange$, operandParameter.value);
                }
                case 'MIN': {
                    (0, expressionFunctionUtils_1.validateColumnType)(operandParameter.column, ['number'], 'MIN', context.adaptableApi);
                    const timeframeChange$ = getTrailingRowValueChange$(dataChangeLog$, timeframeParameter.value);
                    return getDataChangeMin$(dataChangeLog$, timeframeChange$);
                }
                case 'MAX': {
                    (0, expressionFunctionUtils_1.validateColumnType)(operandParameter.column, ['number'], 'MAX', context.adaptableApi);
                    const timeframeChange$ = getTrailingRowValueChange$(dataChangeLog$, timeframeParameter.value);
                    return getDataChangeMax$(dataChangeLog$, timeframeChange$);
                }
                case 'NONE': {
                    return dataChangeLog$.pipe(
                    // wait for the given time
                    (0, operators_1.debounceTime)(timeframeParameter.value), 
                    // completing the observable (ex. alert deletion) will also fire the NONE event
                    // takeUntil takes care of this, ignoring any emissions as soon as the source completes
                    // (count() result is irrelevant here, the main thing is that it emits an source completion)
                    (0, operators_1.takeUntil)(dataChangeLog$.pipe((0, operators_1.count)())));
                }
            }
        },
        returnType: 'boolean',
        description: 'Observes changes with each row in a distinct scope',
        signatures: ['ROW_CHANGE(changeFunction: COUNT|MIN|MAX|NONE, timeframe:TIMEFRAME)'],
        examples: [
            "ROW_CHANGE( COUNT([columnName],2),TIMEFRAME('20s'))",
            "ROW_CHANGE( MAX(COL('columnName')), TIMEFRAME('20s'))",
        ],
        category: 'observable',
    },
    GRID_CHANGE: {
        handler(args, context) {
            const operandParameter = (0, expressionFunctionUtils_1.extractParameter)('GRID_CHANGE', 'operand', ['COUNT', 'MAX', 'MIN', 'NONE'], args);
            const timeframeParameter = (0, expressionFunctionUtils_1.extractParameter)('GRID_CHANGE', 'config', ['TIMEFRAME'], args);
            let dataChangeLog$ = (0, expressionFunctionUtils_1.getDataChangeLog$)(context, operandParameter.column);
            switch (operandParameter.name) {
                case 'COUNT': {
                    const timeframeChange$ = getTrailingGridCountChange$(dataChangeLog$, timeframeParameter.value, operandParameter.value);
                    return getDataChangeCount$(dataChangeLog$, timeframeChange$, operandParameter.value);
                }
                case 'MIN': {
                    (0, expressionFunctionUtils_1.validateColumnType)(operandParameter.column, ['number'], 'MIN', context.adaptableApi);
                    const timeframeChange$ = getTrailingGridValueChange$(dataChangeLog$, timeframeParameter.value);
                    return getDataChangeMin$(dataChangeLog$, timeframeChange$);
                }
                case 'MAX': {
                    (0, expressionFunctionUtils_1.validateColumnType)(operandParameter.column, ['number'], 'MAX', context.adaptableApi);
                    const timeframeChange$ = getTrailingGridValueChange$(dataChangeLog$, timeframeParameter.value);
                    return getDataChangeMax$(dataChangeLog$, timeframeChange$);
                }
                case 'NONE': {
                    return dataChangeLog$.pipe(
                    // add a synthetic first value to ensure the grid is observed even when there are no changes
                    (0, operators_1.startWith)(getDataChangedInfoStub(context)), 
                    // wait for the given time
                    (0, operators_1.debounceTime)(timeframeParameter.value), 
                    // completing the observable (ex. alert deletion) will also fire the NONE event
                    // takeUntil takes care of this, ignoring any emissions as soon as the source completes
                    // (count() result is irrelevant here, the main thing is that it emits a source completion)
                    (0, operators_1.takeUntil)(dataChangeLog$.pipe((0, operators_1.count)())));
                }
            }
        },
        returnType: 'boolean',
        description: 'Observes changes with the entire grid in a single scope',
        signatures: ['GRID_CHANGE(changeFunction: COUNT|MIN|MAX|NONE, timeframe:TIMEFRAME)'],
        examples: [
            "GRID_CHANGE( COUNT([columnName],2),TIMEFRAME('20s'))",
            "GRID_CHANGE( MAX(COL('columnName')), TIMEFRAME('20s'))",
        ],
        category: 'observable',
    },
    ROW_ADDED: {
        handler(args, context) {
            const gridChangeLog$ = (0, expressionFunctionUtils_1.getGridChangeLog$)(context, 'Add');
            return handleGridRowAddedOrRemoved(args, gridChangeLog$, 'ROW_ADDED', context);
        },
        returnType: 'boolean',
        description: 'Observes added rows in the grid',
        signatures: [
            'ROW_ADDED()',
            'ROW_ADDED(rowCount: number)',
            'ROW_ADDED(timeframe:TIMEFRAME)',
            'ROW_ADDED(rowCount: number, timeframe:TIMEFRAME)',
        ],
        examples: [
            'ROW_ADDED()',
            'ROW_ADDED(2)',
            "ROW_ADDED(TIMEFRAME('20s'))",
            "ROW_ADDED(2, TIMEFRAME('20s'))",
        ],
        category: 'observable',
    },
    ROW_REMOVED: {
        handler(args, context) {
            const gridChangeLog$ = (0, expressionFunctionUtils_1.getGridChangeLog$)(context, 'Delete');
            return handleGridRowAddedOrRemoved(args, gridChangeLog$, 'ROW_REMOVED', context);
        },
        returnType: 'boolean',
        description: 'Observes removed rows in the grid',
        signatures: [
            'ROW_REMOVED()',
            'ROW_REMOVED(rowCount: number)',
            'ROW_REMOVED(timeframe:TIMEFRAME)',
            'ROW_REMOVED(rowCount: number, timeframe:TIMEFRAME)',
        ],
        examples: [
            'ROW_REMOVED()',
            'ROW_REMOVED(2)',
            "ROW_REMOVED(TIMEFRAME('20s'))",
            "ROW_REMOVED(2, TIMEFRAME('20s'))",
        ],
        category: 'observable',
    },
    COL: {
        handler(args, context) {
            return (0, expressionFunctionUtils_1.handleColumnFunction)(args, context);
        },
        description: 'References a column by its unique identifier',
        signatures: ['[colName]', 'COL(name: string)'],
        examples: ['[col1]', "COL('col1')"],
        category: 'special',
    },
    COUNT: {
        handler(args) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('COUNT', args);
            const countValue = args.find((arg) => typeof arg === 'number');
            if (countValue == null || countValue <= 0) {
                throw new ExpressionEvaluationError_1.ExpressionEvaluationError('COUNT', 'expects a positive number as argument');
            }
            const result = {
                type: 'operand',
                name: 'COUNT',
                value: countValue,
                column: columnParameter.value,
            };
            return result;
        },
        description: 'Observes if a column value has changed a given number of times.\nValid only as an operand for an observable function.',
        signatures: [
            'COUNT([colName],changeCount: number)',
            'COUNT(COL(name: string),changeCount: number)',
        ],
        examples: ['COUNT([colName],2)', `COUNT(COL('col1'),10)`],
        category: 'operand',
    },
    NONE: {
        handler(args) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('NONE', args);
            const result = {
                type: 'operand',
                name: 'NONE',
                column: columnParameter.value,
            };
            return result;
        },
        description: 'Observes if a column value has NOT changed.\nValid only as an operand for an observable function.',
        signatures: ['NONE([colName])', 'NONE(COL(name: string))'],
        examples: ['NONE([colA])', `NONE(COL('col1'))`],
        category: 'operand',
    },
    MIN: {
        handler(args) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('MIN', args);
            const result = {
                type: 'operand',
                name: 'MIN',
                column: columnParameter.value,
            };
            return result;
        },
        description: 'Observes if the changed column value is the lowest.\nValid only as an operand for an observable function.',
        signatures: ['MIN([colName])', 'MIN(COL(name: string))'],
        examples: ['MIN([colA])', `MIN(COL('col1'))`],
        category: 'operand',
    },
    MAX: {
        handler(args) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('MAX', args);
            const result = {
                type: 'operand',
                name: 'MAX',
                column: columnParameter.value,
            };
            return result;
        },
        description: 'Observes if the changed column value is the highest.\nValid only as an operand for an observable function.',
        signatures: ['MAX([colName])', 'MAX(COL(name: string))'],
        examples: ['MAX([colA])', `MAX(COL('col1'))`],
        category: 'operand',
    },
    TIMEFRAME: {
        handler(args, context) {
            // unit -> milliseconds
            const durationUnitRatios = {
                s: 1000,
                m: 60000,
                h: 3600000,
            };
            const input = args[0] + '';
            //we allow only seconds, minutes & hours durations
            const matchingResult = input.match(TIMEFRAME_REGEX);
            const value = matchingResult?.[1];
            const unit = matchingResult?.[2]?.toLowerCase();
            let duration;
            if (value && unit && durationUnitRatios[unit]) {
                duration = (0, parseInt_1.default)(value) * durationUnitRatios[unit];
            }
            if (!duration) {
                throw new ExpressionEvaluationError_1.ExpressionEvaluationError('TIMEFRAME', `timeframe expression is invalid`);
            }
            // check if the given duration is greater than the generally defined maxTimeframe size
            duration = getMaxTimeframeSize(duration, context);
            const result = {
                type: 'config',
                name: 'TIMEFRAME',
                value: duration,
            };
            return result;
        },
        description: 'Limits the monitoring operators to a trailing timeframe defined in seconds, minutes or hours.\nValid only as an operand for an observable function.',
        signatures: [
            "TIMEFRAME('%numeric_value%s')",
            "TIMEFRAME('%numeric_value%m')",
            "TIMEFRAME('%numeric_value%h')",
        ],
        examples: ["TIMEFRAME('20s')", "TIMEFRAME('5m')", "TIMEFRAME('1h')"],
        category: 'operand',
    },
};
exports.observableExpressionFunctionNames = (0, TypeExtensions_1.getTypedKeys)(exports.observableExpressionFunctions);
// return TRUE if the last(tail) element has the greatest value
const isLastElementMaxValue = (values) => {
    const [tailValue] = values.slice(-1);
    const restValues = values.slice(0, -1);
    return restValues.every((value) => value < tailValue);
};
// return TRUE if the last(tail) element has the greatest value
const isLastElementMinValue = (values) => {
    const [tailValue] = values.slice(-1);
    const restValues = values.slice(0, -1);
    return restValues.every((value) => value > tailValue);
};
// useful for functions which do NOT have an initial change (ex. GRID_CHANGE NONE)
const getDataChangedInfoStub = (context) => {
    let rowNodeStub;
    if (!context.filterFn) {
        rowNodeStub = context.node ?? context.adaptableApi.gridApi.getFirstRowNode();
    }
    else {
        // if there is a WHERE clause defined, find the first rowNode which satisfies the condition
        context.adaptableApi.internalApi.forAllRowNodesDo((rowNode) => {
            if (!rowNodeStub) {
                if (context.filterFn(rowNode)) {
                    rowNodeStub = rowNode;
                }
            }
        });
    }
    const rowNode = rowNodeStub;
    if (rowNode) {
        const primaryKeyValue = context.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(rowNode);
        const columnId = context.adaptableApi.columnApi.internalApi.getQueryableColumnsForUIEditor()[0]?.columnId;
        const oldValue = context.adaptableApi.gridApi.getCellRawValue(primaryKeyValue, columnId);
        const column = context.adaptableApi.columnApi.getColumnWithColumnId(columnId);
        const newValue = oldValue;
        return {
            changedAt: Date.now(),
            rowNode,
            primaryKeyValue,
            column: column,
            oldValue,
            newValue,
        };
    }
    else {
        return {
            changedAt: Date.now(),
        };
    }
};
// returns an observable which fires if the source$ emitted `targetCount` times in the given `timeframeChange$` period
const getDataChangeCount$ = (dataChangeLog$, timeframeChange$, targetCount) => {
    return dataChangeLog$.pipe((0, operators_1.withLatestFrom)(timeframeChange$), (0, operators_1.filter)(([_, changeCountWithinTimeframe]) => {
        return targetCount === changeCountWithinTimeframe;
    }), (0, operators_1.map)(([source]) => source));
};
// returns an observable which fires if the last source$ emission had the lowest(min) value in the given `timeframeChange$` period
const getDataChangeMin$ = (dataChangeLog$, timeframeChange$) => {
    return dataChangeLog$.pipe((0, operators_1.withLatestFrom)(timeframeChange$), (0, operators_1.filter)(([changeLog, values]) => {
        return values.length > 1
            ? isLastElementMinValue(values)
            : (0, expressionFunctionUtils_1.getNumericValue)(changeLog.oldValue) > (0, expressionFunctionUtils_1.getNumericValue)(changeLog.newValue);
    }), (0, operators_1.map)(([changeLog]) => {
        return changeLog;
    }));
};
// returns an observable which fires if the last source$ emission had the highest(max) value in the given `timeframeChange$` period
const getDataChangeMax$ = (dataChangeLog$, timeframeChange$) => {
    return dataChangeLog$.pipe((0, operators_1.withLatestFrom)(timeframeChange$), (0, operators_1.filter)(([changeLog, values]) => {
        return values.length > 1
            ? isLastElementMaxValue(values)
            : (0, expressionFunctionUtils_1.getNumericValue)(changeLog.oldValue) < (0, expressionFunctionUtils_1.getNumericValue)(changeLog.newValue);
    }), (0, operators_1.map)(([changeLog]) => {
        return changeLog;
    }));
};
// returns an observable which maps a dataChangeLogEntry to the number(count) of changed values in the entire grid
// the counter is continuously up-to-date in the given timeframe
// if the given counterLimit is reached, the counter is reset
const getTrailingGridCountChange$ = (source$, trailingPeriod, counterLimit) => {
    return (0, rxjs_1.defer)(() => {
        // keep the counter value in an internal intermediary state
        let counter = 0;
        const movingTimeWindow$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => {
            counter++;
        }, (dataChangeLog) => {
            // counter may have been reset during the timeframe, in which case we skip the decrement
            if (counter > 0) {
                counter--;
            }
        });
        return movingTimeWindow$.pipe((0, operators_1.map)(() => {
            const gridCounter = counter;
            if (gridCounter === counterLimit) {
                // reset counter
                counter = 0;
            }
            return gridCounter;
        }));
    });
};
// returns an observable which maps a dataChangeLogEntry to the number(count) of changed values in the row of the given dataChangeLogEntry
// the counter is continuously up-to-date in the given timeframe
// if the given counterLimit is reached, the counter is reset
const getTrailingRowCountChange$ = (source$, trailingPeriod, counterLimit) => {
    return (0, rxjs_1.defer)(() => {
        // keep the counter value in an internal intermediary state (distinct per row PK)
        const counterMap = new Map();
        const getCellCounter = (dataChangeLog) => counterMap.get(dataChangeLog.primaryKeyValue) ?? 0;
        const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => {
            let currentCounter = getCellCounter(dataChangeLog);
            counterMap.set(dataChangeLog.primaryKeyValue, ++currentCounter);
        }, (dataChangeLog) => {
            let currentCounter = getCellCounter(dataChangeLog);
            // counter may have been reset during the timeframe, in which case we skip the decrement
            if (currentCounter > 0) {
                counterMap.set(dataChangeLog.primaryKeyValue, --currentCounter);
            }
        });
        return slidingTimeframe$.pipe((0, operators_1.map)((dataChangeLog) => {
            const cellCounter = getCellCounter(dataChangeLog);
            if (cellCounter === counterLimit) {
                // reset counter
                counterMap.set(dataChangeLog.primaryKeyValue, 0);
            }
            return cellCounter;
        }));
    });
};
// returns an observable which maps a dataChangeLogEntry to the array of changed values in the entire grid
// the changed values array is continuously up-to-date in the given timeframe
const getTrailingGridValueChange$ = (source$, trailingPeriod) => {
    return (0, rxjs_1.defer)(() => {
        // keep the changed values in an internal intermediary state
        let values = [];
        const doubleInsertionsMap = new WeakMap();
        const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => {
            if (!values.length) {
                // values is empty, so we evaluate both old & new node values
                values.push((0, expressionFunctionUtils_1.getNumericValue)(dataChangeLog.oldValue));
                // mark the double insertion, we will have to pop 2 elements
                doubleInsertionsMap.set(dataChangeLog, true);
            }
            values.push((0, expressionFunctionUtils_1.getNumericValue)(dataChangeLog.newValue));
        }, (dataChangeLog) => {
            values.shift();
            if (doubleInsertionsMap.get(dataChangeLog)) {
                // the current change inserted both old and new values, so we have to extract 2 elements
                values.shift();
                doubleInsertionsMap.delete(dataChangeLog);
            }
        });
        return slidingTimeframe$.pipe((0, operators_1.map)(() => values));
    });
};
// returns an observable which maps a dataChangeLogEntry to the array of changed values in the row of the given dataChangeLogEntry
// the changed values array is continuously up-to-date in the given timeframe
const getTrailingRowValueChange$ = (source$, trailingPeriod) => {
    return (0, rxjs_1.defer)(() => {
        // keep the changed values in an internal intermediary state (distinct per row PK)
        const rowValuesMap = new Map();
        const doubleInsertionsMap = new WeakMap();
        const getRowValues = (dataChangeLog) => rowValuesMap.get(dataChangeLog.primaryKeyValue) ?? [];
        const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => {
            let rowValues = getRowValues(dataChangeLog);
            if (!rowValues.length) {
                // values is empty, so we evaluate both old & new node values
                rowValues.push((0, expressionFunctionUtils_1.getNumericValue)(dataChangeLog.oldValue));
                // mark the double insertion, we will have to pop 2 elements
                doubleInsertionsMap.set(dataChangeLog, true);
            }
            rowValues.push((0, expressionFunctionUtils_1.getNumericValue)(dataChangeLog.newValue));
            rowValuesMap.set(dataChangeLog.primaryKeyValue, rowValues);
        }, (dataChangeLog) => {
            let rowValues = getRowValues(dataChangeLog);
            rowValues.shift();
            if (doubleInsertionsMap.get(dataChangeLog)) {
                // the current change inserted both old and new values, so we have to extract 2 elements
                rowValues.shift();
                doubleInsertionsMap.delete(dataChangeLog);
            }
            rowValuesMap.set(dataChangeLog.primaryKeyValue, rowValues);
        });
        return slidingTimeframe$.pipe((0, operators_1.map)((dataChangeLog) => getRowValues(dataChangeLog)));
    });
};
// return an observable which will emit when the source$ event enters, respectively exits the timeframe
// it also executes the provided onEnter/onExit handlers
const getSlidingTimeframe$ = (source$, timeframeDuration, onTimeframeEnter, onTimeframeExit) => {
    return source$.pipe(
    // create intermediary observable which, for each emission from source$:
    (0, operators_1.mergeMap)((dataChangeLog) => {
        // 1. it will emit the payload immediately...
        const enter$ = (0, rxjs_1.of)(dataChangeLog).pipe(
        // ...and execute the provided onPushHandler() callback
        (0, operators_1.tap)((dataChangeLog) => onTimeframeEnter(dataChangeLog)));
        // 2. and after a given 'timeWindowSize' delay it will re-emit the payload...
        const exit$ = (0, rxjs_1.of)(dataChangeLog).pipe((0, operators_1.delay)(timeframeDuration), 
        // ...and execute the provided onPopHandler() callback
        (0, operators_1.tap)((dataChangeLog) => onTimeframeExit(dataChangeLog)));
        return (0, rxjs_1.of)(enter$, exit$).pipe((0, operators_1.mergeAll)());
    }));
};
const getMaxTimeframeSize = (expressionValue, context) => {
    let maxTimeframeSize = context.adaptableApi.optionsApi.getExpressionOptions().maxTimeframeSize;
    if (maxTimeframeSize > SYSTEM_MAX_TIMEFRAME_SIZE) {
        maxTimeframeSize = SYSTEM_MAX_TIMEFRAME_SIZE;
    }
    return expressionValue > maxTimeframeSize ? maxTimeframeSize : expressionValue;
};
const handleGridRowAddedOrRemoved = (args, gridChangeLog$, consumingFunction, context) => {
    let countValue = args.find((arg) => typeof arg === 'number');
    if (countValue < 0) {
        throw new ExpressionEvaluationError_1.ExpressionEvaluationError(consumingFunction, 'supports only zero or a positive number as argument');
    }
    let timeframeParameter = (0, expressionFunctionUtils_1.extractParameter)(consumingFunction, 'config', ['TIMEFRAME'], args, { isOptional: true });
    if (countValue === 0 && timeframeParameter == null) {
        throw new ExpressionEvaluationError_1.ExpressionEvaluationError(consumingFunction, 'requires a TIMEFRAME parameter when observing for no changes');
    }
    if (countValue == null && timeframeParameter == null) {
        // default - return all new rows
        return gridChangeLog$;
    }
    if (countValue == null) {
        // default count value of 1
        countValue = 1;
    }
    if (timeframeParameter == null) {
        // default time parameter of max
        timeframeParameter = {
            name: 'TIMEFRAME',
            type: 'config',
            value: SYSTEM_MAX_TIMEFRAME_SIZE,
        };
    }
    if (countValue === 0) {
        // handle special case when observing NO changes
        const rowDataChangeInfoStub = {
            rowTrigger: consumingFunction === 'ROW_ADDED' ? 'Add' : 'Delete',
            rowNodes: [],
            dataRows: [],
            changedAt: Date.now(),
            ...context.adaptableApi.internalApi.buildBaseContext(),
        };
        return gridChangeLog$.pipe(
        // add a synthetic first value to ensure the grid is observed even when there are no changes
        (0, operators_1.startWith)(rowDataChangeInfoStub), 
        // wait for the given time
        (0, operators_1.debounceTime)(timeframeParameter.value), 
        // completing the observable (ex. alert deletion) will also fire the NONE event
        // takeUntil takes care of this, ignoring any emissions as soon as the source completes
        // (count() result is irrelevant here, the main thing is that it emits a source completion)
        (0, operators_1.takeUntil)(gridChangeLog$.pipe((0, operators_1.count)())));
    }
    const timeframeChange$ = getTrailingGridCountChange$(gridChangeLog$, timeframeParameter.value, countValue);
    return getDataChangeCount$(gridChangeLog$, timeframeChange$, countValue);
};
