203 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
/*
 | 
						|
Copyright 2018 New Vector Ltd
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
export class DecryptionFailure {
 | 
						|
    constructor(failedEventId, errorCode) {
 | 
						|
        this.failedEventId = failedEventId;
 | 
						|
        this.errorCode = errorCode;
 | 
						|
        this.ts = Date.now();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class DecryptionFailureTracker {
 | 
						|
    // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
 | 
						|
    // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
 | 
						|
    // are accumulated in `failureCounts`.
 | 
						|
    failures = [];
 | 
						|
 | 
						|
    // A histogram of the number of failures that will be tracked at the next tracking
 | 
						|
    // interval, split by failure error code.
 | 
						|
    failureCounts = {
 | 
						|
        // [errorCode]: 42
 | 
						|
    };
 | 
						|
 | 
						|
    // Event IDs of failures that were tracked previously
 | 
						|
    trackedEventHashMap = {
 | 
						|
        // [eventId]: true
 | 
						|
    };
 | 
						|
 | 
						|
    // Set to an interval ID when `start` is called
 | 
						|
    checkInterval = null;
 | 
						|
    trackInterval = null;
 | 
						|
 | 
						|
    // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
 | 
						|
    static TRACK_INTERVAL_MS = 60000;
 | 
						|
 | 
						|
    // Call `checkFailures` every `CHECK_INTERVAL_MS`.
 | 
						|
    static CHECK_INTERVAL_MS = 5000;
 | 
						|
 | 
						|
    // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting
 | 
						|
    // the failure in `failureCounts`.
 | 
						|
    static GRACE_PERIOD_MS = 60000;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a new DecryptionFailureTracker.
 | 
						|
     *
 | 
						|
     * Call `eventDecrypted(event, err)` on this instance when an event is decrypted.
 | 
						|
     *
 | 
						|
     * Call `start()` to start the tracker, and `stop()` to stop tracking.
 | 
						|
     *
 | 
						|
     * @param {function} fn The tracking function, which will be called when failures
 | 
						|
     * are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`,
 | 
						|
     * where `count` is the number of failures and `errorCode` matches the `.code` of
 | 
						|
     * provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified.
 | 
						|
     * @param {function?} errorCodeMapFn The function used to map error codes to the
 | 
						|
     * trackedErrorCode. If not provided, the `.code` of errors will be used.
 | 
						|
     */
 | 
						|
    constructor(fn, errorCodeMapFn) {
 | 
						|
        if (!fn || typeof fn !== 'function') {
 | 
						|
            throw new Error('DecryptionFailureTracker requires tracking function');
 | 
						|
        }
 | 
						|
 | 
						|
        if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
 | 
						|
            throw new Error('DecryptionFailureTracker second constructor argument should be a function');
 | 
						|
        }
 | 
						|
 | 
						|
        this._trackDecryptionFailure = fn;
 | 
						|
        this._mapErrorCode = errorCodeMapFn;
 | 
						|
    }
 | 
						|
 | 
						|
    // loadTrackedEventHashMap() {
 | 
						|
    //     this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {};
 | 
						|
    // }
 | 
						|
 | 
						|
    // saveTrackedEventHashMap() {
 | 
						|
    //     localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
 | 
						|
    // }
 | 
						|
 | 
						|
    eventDecrypted(e, err) {
 | 
						|
        if (err) {
 | 
						|
            this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
 | 
						|
        } else {
 | 
						|
            // Could be an event in the failures, remove it
 | 
						|
            this.removeDecryptionFailuresForEvent(e);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    addDecryptionFailure(failure) {
 | 
						|
        this.failures.push(failure);
 | 
						|
    }
 | 
						|
 | 
						|
    removeDecryptionFailuresForEvent(e) {
 | 
						|
        this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Start checking for and tracking failures.
 | 
						|
     */
 | 
						|
    start() {
 | 
						|
        this.checkInterval = setInterval(
 | 
						|
            () => this.checkFailures(Date.now()),
 | 
						|
            DecryptionFailureTracker.CHECK_INTERVAL_MS,
 | 
						|
        );
 | 
						|
 | 
						|
        this.trackInterval = setInterval(
 | 
						|
            () => this.trackFailures(),
 | 
						|
            DecryptionFailureTracker.TRACK_INTERVAL_MS,
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Clear state and stop checking for and tracking failures.
 | 
						|
     */
 | 
						|
    stop() {
 | 
						|
        clearInterval(this.checkInterval);
 | 
						|
        clearInterval(this.trackInterval);
 | 
						|
 | 
						|
        this.failures = [];
 | 
						|
        this.failureCounts = {};
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
 | 
						|
     * tracked. Only mark one failure per event ID.
 | 
						|
     * @param {number} nowTs the timestamp that represents the time now.
 | 
						|
     */
 | 
						|
    checkFailures(nowTs) {
 | 
						|
        const failuresGivenGrace = [];
 | 
						|
        const failuresNotReady = [];
 | 
						|
        while (this.failures.length > 0) {
 | 
						|
            const f = this.failures.shift();
 | 
						|
            if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
 | 
						|
                failuresGivenGrace.push(f);
 | 
						|
            } else {
 | 
						|
                failuresNotReady.push(f);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        this.failures = failuresNotReady;
 | 
						|
 | 
						|
        // Only track one failure per event
 | 
						|
        const dedupedFailuresMap = failuresGivenGrace.reduce(
 | 
						|
            (map, failure) => {
 | 
						|
                if (!this.trackedEventHashMap[failure.failedEventId]) {
 | 
						|
                    return map.set(failure.failedEventId, failure);
 | 
						|
                } else {
 | 
						|
                    return map;
 | 
						|
                }
 | 
						|
            },
 | 
						|
            // Use a map to preseve key ordering
 | 
						|
            new Map(),
 | 
						|
        );
 | 
						|
 | 
						|
        const trackedEventIds = [...dedupedFailuresMap.keys()];
 | 
						|
 | 
						|
        this.trackedEventHashMap = trackedEventIds.reduce(
 | 
						|
            (result, eventId) => ({...result, [eventId]: true}),
 | 
						|
            this.trackedEventHashMap,
 | 
						|
        );
 | 
						|
 | 
						|
        // Commented out for now for expediency, we need to consider unbound nature of storing
 | 
						|
        // this in localStorage
 | 
						|
        // this.saveTrackedEventHashMap();
 | 
						|
 | 
						|
        const dedupedFailures = dedupedFailuresMap.values();
 | 
						|
 | 
						|
        this._aggregateFailures(dedupedFailures);
 | 
						|
    }
 | 
						|
 | 
						|
    _aggregateFailures(failures) {
 | 
						|
        for (const failure of failures) {
 | 
						|
            const errorCode = failure.errorCode;
 | 
						|
            this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * If there are failures that should be tracked, call the given trackDecryptionFailure
 | 
						|
     * function with the number of failures that should be tracked.
 | 
						|
     */
 | 
						|
    trackFailures() {
 | 
						|
        for (const errorCode of Object.keys(this.failureCounts)) {
 | 
						|
            if (this.failureCounts[errorCode] > 0) {
 | 
						|
                const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
 | 
						|
 | 
						|
                this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
 | 
						|
                this.failureCounts[errorCode] = 0;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |