1/**
2 * Copyright (c) 650 Industries.
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8import * as React from 'react';
9import NativeLogBox from '../modules/NativeLogBox';
10import parseErrorStack from '../modules/parseErrorStack';
11import { LogBoxLog } from './LogBoxLog';
12import { LogContext } from './LogContext';
13import { parseLogBoxException } from './parseLogBoxLog';
14const observers = new Set();
15const ignorePatterns = new Set();
16let logs = new Set();
17let updateTimeout = null;
18let _isDisabled = false;
19let _selectedIndex = -1;
20const LOGBOX_ERROR_MESSAGE = 'An error was thrown when attempting to render log messages via LogBox.';
21function getNextState() {
22    return {
23        logs,
24        isDisabled: _isDisabled,
25        selectedLogIndex: _selectedIndex,
26    };
27}
28export function reportLogBoxError(error, componentStack) {
29    const ExceptionsManager = require('../modules/ExceptionsManager').default;
30    if (componentStack != null) {
31        error.componentStack = componentStack;
32    }
33    ExceptionsManager.handleException(error);
34}
35export function reportUnexpectedLogBoxError(error, componentStack) {
36    error.message = `${LOGBOX_ERROR_MESSAGE}\n\n${error.message}`;
37    return reportLogBoxError(error, componentStack);
38}
39export function isLogBoxErrorMessage(message) {
40    return typeof message === 'string' && message.includes(LOGBOX_ERROR_MESSAGE);
41}
42export function isMessageIgnored(message) {
43    for (const pattern of ignorePatterns) {
44        if ((pattern instanceof RegExp && pattern.test(message)) ||
45            (typeof pattern === 'string' && message.includes(pattern))) {
46            return true;
47        }
48    }
49    return false;
50}
51function setImmediateShim(callback) {
52    if (!global.setImmediate) {
53        return setTimeout(callback, 0);
54    }
55    return global.setImmediate(callback);
56}
57function handleUpdate() {
58    if (updateTimeout == null) {
59        updateTimeout = setImmediateShim(() => {
60            updateTimeout = null;
61            const nextState = getNextState();
62            observers.forEach(({ observer }) => observer(nextState));
63        });
64    }
65}
66function appendNewLog(newLog) {
67    // Don't want store these logs because they trigger a
68    // state update when we add them to the store.
69    if (isMessageIgnored(newLog.message.content)) {
70        return;
71    }
72    // If the next log has the same category as the previous one
73    // then roll it up into the last log in the list by incrementing
74    // the count (similar to how Chrome does it).
75    const lastLog = Array.from(logs).pop();
76    if (lastLog && lastLog.category === newLog.category) {
77        lastLog.incrementCount();
78        handleUpdate();
79        return;
80    }
81    if (newLog.level === 'fatal') {
82        // If possible, to avoid jank, we don't want to open the error before
83        // it's symbolicated. To do that, we optimistically wait for
84        // symbolication for up to a second before adding the log.
85        const OPTIMISTIC_WAIT_TIME = 1000;
86        let addPendingLog = () => {
87            logs.add(newLog);
88            if (_selectedIndex < 0) {
89                setSelectedLog(logs.size - 1);
90            }
91            else {
92                handleUpdate();
93            }
94            addPendingLog = null;
95        };
96        const optimisticTimeout = setTimeout(() => {
97            if (addPendingLog) {
98                addPendingLog();
99            }
100        }, OPTIMISTIC_WAIT_TIME);
101        // TODO: HANDLE THIS
102        newLog.symbolicate('component');
103        newLog.symbolicate('stack', (status) => {
104            if (addPendingLog && status !== 'PENDING') {
105                addPendingLog();
106                clearTimeout(optimisticTimeout);
107            }
108            else if (status !== 'PENDING') {
109                // The log has already been added but we need to trigger a render.
110                handleUpdate();
111            }
112        });
113    }
114    else if (newLog.level === 'syntax') {
115        logs.add(newLog);
116        setSelectedLog(logs.size - 1);
117    }
118    else {
119        logs.add(newLog);
120        handleUpdate();
121    }
122}
123export function addLog(log) {
124    const errorForStackTrace = new Error();
125    // Parsing logs are expensive so we schedule this
126    // otherwise spammy logs would pause rendering.
127    setImmediate(() => {
128        try {
129            const stack = parseErrorStack(errorForStackTrace?.stack);
130            appendNewLog(new LogBoxLog({
131                level: log.level,
132                message: log.message,
133                isComponentError: false,
134                stack,
135                category: log.category,
136                componentStack: log.componentStack,
137            }));
138        }
139        catch (error) {
140            reportUnexpectedLogBoxError(error);
141        }
142    });
143}
144export function addException(error) {
145    // Parsing logs are expensive so we schedule this
146    // otherwise spammy logs would pause rendering.
147    setImmediate(() => {
148        try {
149            appendNewLog(new LogBoxLog(parseLogBoxException(error)));
150        }
151        catch (loggingError) {
152            reportUnexpectedLogBoxError(loggingError);
153        }
154    });
155}
156export function symbolicateLogNow(type, log) {
157    log.symbolicate(type, () => {
158        handleUpdate();
159    });
160}
161export function retrySymbolicateLogNow(type, log) {
162    log.retrySymbolicate(type, () => {
163        handleUpdate();
164    });
165}
166export function symbolicateLogLazy(type, log) {
167    log.symbolicate(type);
168}
169export function clear() {
170    if (logs.size > 0) {
171        logs = new Set();
172        setSelectedLog(-1);
173    }
174}
175export function setSelectedLog(proposedNewIndex) {
176    const oldIndex = _selectedIndex;
177    let newIndex = proposedNewIndex;
178    const logArray = Array.from(logs);
179    let index = logArray.length - 1;
180    while (index >= 0) {
181        // The latest syntax error is selected and displayed before all other logs.
182        if (logArray[index].level === 'syntax') {
183            newIndex = index;
184            break;
185        }
186        index -= 1;
187    }
188    _selectedIndex = newIndex;
189    handleUpdate();
190    if (NativeLogBox) {
191        setTimeout(() => {
192            if (oldIndex < 0 && newIndex >= 0) {
193                NativeLogBox.show();
194            }
195            else if (oldIndex >= 0 && newIndex < 0) {
196                NativeLogBox.hide();
197            }
198        }, 0);
199    }
200}
201export function clearWarnings() {
202    const newLogs = Array.from(logs).filter((log) => log.level !== 'warn');
203    if (newLogs.length !== logs.size) {
204        logs = new Set(newLogs);
205        setSelectedLog(-1);
206        handleUpdate();
207    }
208}
209export function clearErrors() {
210    const newLogs = Array.from(logs).filter((log) => log.level !== 'error' && log.level !== 'fatal');
211    if (newLogs.length !== logs.size) {
212        logs = new Set(newLogs);
213        setSelectedLog(-1);
214    }
215}
216export function dismiss(log) {
217    if (logs.has(log)) {
218        logs.delete(log);
219        handleUpdate();
220    }
221}
222export function getIgnorePatterns() {
223    return Array.from(ignorePatterns);
224}
225export function addIgnorePatterns(patterns) {
226    const existingSize = ignorePatterns.size;
227    // The same pattern may be added multiple times, but adding a new pattern
228    // can be expensive so let's find only the ones that are new.
229    patterns.forEach((pattern) => {
230        if (pattern instanceof RegExp) {
231            for (const existingPattern of ignorePatterns) {
232                if (existingPattern instanceof RegExp &&
233                    existingPattern.toString() === pattern.toString()) {
234                    return;
235                }
236            }
237            ignorePatterns.add(pattern);
238        }
239        ignorePatterns.add(pattern);
240    });
241    if (ignorePatterns.size === existingSize) {
242        return;
243    }
244    // We need to recheck all of the existing logs.
245    // This allows adding an ignore pattern anywhere in the codebase.
246    // Without this, if you ignore a pattern after the a log is created,
247    // then we would keep showing the log.
248    logs = new Set(Array.from(logs).filter((log) => !isMessageIgnored(log.message.content)));
249    handleUpdate();
250}
251export function setDisabled(value) {
252    if (value === _isDisabled) {
253        return;
254    }
255    _isDisabled = value;
256    handleUpdate();
257}
258export function isDisabled() {
259    return _isDisabled;
260}
261export function observe(observer) {
262    const subscription = { observer };
263    observers.add(subscription);
264    observer(getNextState());
265    return {
266        unsubscribe() {
267            observers.delete(subscription);
268        },
269    };
270}
271export function withSubscription(WrappedComponent) {
272    class LogBoxStateSubscription extends React.Component {
273        static getDerivedStateFromError() {
274            return { hasError: true };
275        }
276        componentDidCatch(err, errorInfo) {
277            /* $FlowFixMe[class-object-subtyping] added when improving typing for
278             * this parameters */
279            reportLogBoxError(err, errorInfo.componentStack);
280        }
281        _subscription;
282        state = {
283            logs: new Set(),
284            isDisabled: false,
285            hasError: false,
286            selectedLogIndex: -1,
287        };
288        render() {
289            if (this.state.hasError) {
290                // This happens when the component failed to render, in which case we delegate to the native redbox.
291                // We can't show any fallback UI here, because the error may be with <View> or <Text>.
292                return null;
293            }
294            return (React.createElement(LogContext.Provider, { value: {
295                    selectedLogIndex: this.state.selectedLogIndex,
296                    isDisabled: this.state.isDisabled,
297                    logs: Array.from(this.state.logs),
298                } },
299                this.props.children,
300                React.createElement(WrappedComponent, null)));
301        }
302        componentDidMount() {
303            this._subscription = observe((data) => {
304                this.setState(data);
305            });
306        }
307        componentWillUnmount() {
308            if (this._subscription != null) {
309                this._subscription.unsubscribe();
310            }
311        }
312        _handleDismiss = () => {
313            // Here we handle the cases when the log is dismissed and it
314            // was either the last log, or when the current index
315            // is now outside the bounds of the log array.
316            const { selectedLogIndex, logs: stateLogs } = this.state;
317            const logsArray = Array.from(stateLogs);
318            if (selectedLogIndex != null) {
319                if (logsArray.length - 1 <= 0) {
320                    setSelectedLog(-1);
321                }
322                else if (selectedLogIndex >= logsArray.length - 1) {
323                    setSelectedLog(selectedLogIndex - 1);
324                }
325                dismiss(logsArray[selectedLogIndex]);
326            }
327        };
328        _handleMinimize = () => {
329            setSelectedLog(-1);
330        };
331        _handleSetSelectedLog = (index) => {
332            setSelectedLog(index);
333        };
334    }
335    // @ts-expect-error
336    return LogBoxStateSubscription;
337}
338//# sourceMappingURL=LogBoxData.js.map