1*26ad19fcSEvan Bacon/**
2*26ad19fcSEvan Bacon * Copyright (c) 650 Industries.
3*26ad19fcSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates.
4*26ad19fcSEvan Bacon *
5*26ad19fcSEvan Bacon * This source code is licensed under the MIT license found in the
6*26ad19fcSEvan Bacon * LICENSE file in the root directory of this source tree.
7*26ad19fcSEvan Bacon */
8*26ad19fcSEvan Bacon
9*26ad19fcSEvan Baconimport * as LogBoxSymbolication from './LogBoxSymbolication';
10*26ad19fcSEvan Baconimport type { Stack } from './LogBoxSymbolication';
11*26ad19fcSEvan Baconimport type { Category, Message, ComponentStack, CodeFrame } from './parseLogBoxLog';
12*26ad19fcSEvan Bacon
13*26ad19fcSEvan Bacontype SymbolicationStatus = 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED';
14*26ad19fcSEvan Bacon
15*26ad19fcSEvan Baconexport type LogLevel = 'warn' | 'error' | 'fatal' | 'syntax' | 'static';
16*26ad19fcSEvan Bacon
17*26ad19fcSEvan Baconexport type LogBoxLogData = {
18*26ad19fcSEvan Bacon  level: LogLevel;
19*26ad19fcSEvan Bacon  type?: string;
20*26ad19fcSEvan Bacon  message: Message;
21*26ad19fcSEvan Bacon  stack: Stack;
22*26ad19fcSEvan Bacon  category: string;
23*26ad19fcSEvan Bacon  componentStack: ComponentStack;
24*26ad19fcSEvan Bacon  codeFrame?: CodeFrame;
25*26ad19fcSEvan Bacon  isComponentError: boolean;
26*26ad19fcSEvan Bacon};
27*26ad19fcSEvan Bacon
28*26ad19fcSEvan Baconexport type StackType = 'stack' | 'component';
29*26ad19fcSEvan Bacon
30*26ad19fcSEvan Baconfunction componentStackToStack(componentStack: ComponentStack): Stack {
31*26ad19fcSEvan Bacon  return componentStack.map((stack) => ({
32*26ad19fcSEvan Bacon    file: stack.fileName,
33*26ad19fcSEvan Bacon    methodName: stack.content,
34*26ad19fcSEvan Bacon    lineNumber: stack.location?.row ?? 0,
35*26ad19fcSEvan Bacon    column: stack.location?.column ?? 0,
36*26ad19fcSEvan Bacon    arguments: [],
37*26ad19fcSEvan Bacon  }));
38*26ad19fcSEvan Bacon}
39*26ad19fcSEvan Bacon
40*26ad19fcSEvan Bacontype SymbolicationCallback = (status: SymbolicationStatus) => void;
41*26ad19fcSEvan Bacon
42*26ad19fcSEvan Bacontype SymbolicationResult =
43*26ad19fcSEvan Bacon  | { error: null; stack: null; status: 'NONE' }
44*26ad19fcSEvan Bacon  | { error: null; stack: null; status: 'PENDING' }
45*26ad19fcSEvan Bacon  | { error: null; stack: Stack; status: 'COMPLETE' }
46*26ad19fcSEvan Bacon  | { error: Error; stack: null; status: 'FAILED' };
47*26ad19fcSEvan Bacon
48*26ad19fcSEvan Baconexport class LogBoxLog {
49*26ad19fcSEvan Bacon  message: Message;
50*26ad19fcSEvan Bacon  type: string;
51*26ad19fcSEvan Bacon  category: Category;
52*26ad19fcSEvan Bacon  componentStack: ComponentStack;
53*26ad19fcSEvan Bacon  stack: Stack;
54*26ad19fcSEvan Bacon  count: number;
55*26ad19fcSEvan Bacon  level: LogLevel;
56*26ad19fcSEvan Bacon  codeFrame?: CodeFrame;
57*26ad19fcSEvan Bacon  isComponentError: boolean;
58*26ad19fcSEvan Bacon  symbolicated: Record<StackType, SymbolicationResult> = {
59*26ad19fcSEvan Bacon    stack: {
60*26ad19fcSEvan Bacon      error: null,
61*26ad19fcSEvan Bacon      stack: null,
62*26ad19fcSEvan Bacon      status: 'NONE',
63*26ad19fcSEvan Bacon    },
64*26ad19fcSEvan Bacon    component: {
65*26ad19fcSEvan Bacon      error: null,
66*26ad19fcSEvan Bacon      stack: null,
67*26ad19fcSEvan Bacon      status: 'NONE',
68*26ad19fcSEvan Bacon    },
69*26ad19fcSEvan Bacon  };
70*26ad19fcSEvan Bacon
71*26ad19fcSEvan Bacon  private callbacks: Map<StackType, Set<SymbolicationCallback>> = new Map();
72*26ad19fcSEvan Bacon
73*26ad19fcSEvan Bacon  constructor(
74*26ad19fcSEvan Bacon    data: LogBoxLogData & {
75*26ad19fcSEvan Bacon      symbolicated?: Record<StackType, SymbolicationResult>;
76*26ad19fcSEvan Bacon    }
77*26ad19fcSEvan Bacon  ) {
78*26ad19fcSEvan Bacon    this.level = data.level;
79*26ad19fcSEvan Bacon    this.type = data.type ?? 'error';
80*26ad19fcSEvan Bacon    this.message = data.message;
81*26ad19fcSEvan Bacon    this.stack = data.stack;
82*26ad19fcSEvan Bacon    this.category = data.category;
83*26ad19fcSEvan Bacon    this.componentStack = data.componentStack;
84*26ad19fcSEvan Bacon    this.codeFrame = data.codeFrame;
85*26ad19fcSEvan Bacon    this.isComponentError = data.isComponentError;
86*26ad19fcSEvan Bacon    this.count = 1;
87*26ad19fcSEvan Bacon    this.symbolicated = data.symbolicated ?? this.symbolicated;
88*26ad19fcSEvan Bacon  }
89*26ad19fcSEvan Bacon
90*26ad19fcSEvan Bacon  incrementCount(): void {
91*26ad19fcSEvan Bacon    this.count += 1;
92*26ad19fcSEvan Bacon  }
93*26ad19fcSEvan Bacon
94*26ad19fcSEvan Bacon  getAvailableStack(type: StackType): Stack | null {
95*26ad19fcSEvan Bacon    if (this.symbolicated[type].status === 'COMPLETE') {
96*26ad19fcSEvan Bacon      return this.symbolicated[type].stack;
97*26ad19fcSEvan Bacon    }
98*26ad19fcSEvan Bacon    return this.getStack(type);
99*26ad19fcSEvan Bacon  }
100*26ad19fcSEvan Bacon
101*26ad19fcSEvan Bacon  private flushCallbacks(type: StackType): void {
102*26ad19fcSEvan Bacon    const callbacks = this.callbacks.get(type);
103*26ad19fcSEvan Bacon    const status = this.symbolicated[type].status;
104*26ad19fcSEvan Bacon    if (callbacks) {
105*26ad19fcSEvan Bacon      for (const callback of callbacks) {
106*26ad19fcSEvan Bacon        callback(status);
107*26ad19fcSEvan Bacon      }
108*26ad19fcSEvan Bacon      callbacks.clear();
109*26ad19fcSEvan Bacon    }
110*26ad19fcSEvan Bacon  }
111*26ad19fcSEvan Bacon
112*26ad19fcSEvan Bacon  private pushCallback(type: StackType, callback: SymbolicationCallback): void {
113*26ad19fcSEvan Bacon    let callbacks = this.callbacks.get(type);
114*26ad19fcSEvan Bacon    if (!callbacks) {
115*26ad19fcSEvan Bacon      callbacks = new Set();
116*26ad19fcSEvan Bacon      this.callbacks.set(type, callbacks);
117*26ad19fcSEvan Bacon    }
118*26ad19fcSEvan Bacon    callbacks.add(callback);
119*26ad19fcSEvan Bacon  }
120*26ad19fcSEvan Bacon
121*26ad19fcSEvan Bacon  retrySymbolicate(type: StackType, callback?: (status: SymbolicationStatus) => void): void {
122*26ad19fcSEvan Bacon    this._symbolicate(type, true, callback);
123*26ad19fcSEvan Bacon  }
124*26ad19fcSEvan Bacon
125*26ad19fcSEvan Bacon  symbolicate(type: StackType, callback?: (status: SymbolicationStatus) => void): void {
126*26ad19fcSEvan Bacon    this._symbolicate(type, false, callback);
127*26ad19fcSEvan Bacon  }
128*26ad19fcSEvan Bacon
129*26ad19fcSEvan Bacon  private _symbolicate(
130*26ad19fcSEvan Bacon    type: StackType,
131*26ad19fcSEvan Bacon    retry: boolean,
132*26ad19fcSEvan Bacon    callback?: (status: SymbolicationStatus) => void
133*26ad19fcSEvan Bacon  ): void {
134*26ad19fcSEvan Bacon    if (callback) {
135*26ad19fcSEvan Bacon      this.pushCallback(type, callback);
136*26ad19fcSEvan Bacon    }
137*26ad19fcSEvan Bacon    const status = this.symbolicated[type].status;
138*26ad19fcSEvan Bacon
139*26ad19fcSEvan Bacon    if (status === 'COMPLETE') {
140*26ad19fcSEvan Bacon      return this.flushCallbacks(type);
141*26ad19fcSEvan Bacon    }
142*26ad19fcSEvan Bacon
143*26ad19fcSEvan Bacon    if (retry) {
144*26ad19fcSEvan Bacon      LogBoxSymbolication.deleteStack(this.getStack(type));
145*26ad19fcSEvan Bacon      this.handleSymbolicate(type);
146*26ad19fcSEvan Bacon    } else {
147*26ad19fcSEvan Bacon      if (status === 'NONE') {
148*26ad19fcSEvan Bacon        this.handleSymbolicate(type);
149*26ad19fcSEvan Bacon      }
150*26ad19fcSEvan Bacon    }
151*26ad19fcSEvan Bacon  }
152*26ad19fcSEvan Bacon
153*26ad19fcSEvan Bacon  private componentStackCache: Stack | null = null;
154*26ad19fcSEvan Bacon
155*26ad19fcSEvan Bacon  private getStack(type: StackType): Stack {
156*26ad19fcSEvan Bacon    if (type === 'component') {
157*26ad19fcSEvan Bacon      if (this.componentStackCache == null) {
158*26ad19fcSEvan Bacon        this.componentStackCache = componentStackToStack(this.componentStack);
159*26ad19fcSEvan Bacon      }
160*26ad19fcSEvan Bacon      return this.componentStackCache;
161*26ad19fcSEvan Bacon    }
162*26ad19fcSEvan Bacon    return this.stack;
163*26ad19fcSEvan Bacon  }
164*26ad19fcSEvan Bacon
165*26ad19fcSEvan Bacon  private handleSymbolicate(type: StackType): void {
166*26ad19fcSEvan Bacon    if (type === 'component' && !this.componentStack?.length) {
167*26ad19fcSEvan Bacon      return;
168*26ad19fcSEvan Bacon    }
169*26ad19fcSEvan Bacon
170*26ad19fcSEvan Bacon    if (this.symbolicated[type].status !== 'PENDING') {
171*26ad19fcSEvan Bacon      this.updateStatus(type, null, null, null);
172*26ad19fcSEvan Bacon      LogBoxSymbolication.symbolicate(this.getStack(type)).then(
173*26ad19fcSEvan Bacon        (data) => {
174*26ad19fcSEvan Bacon          this.updateStatus(type, null, data?.stack, data?.codeFrame);
175*26ad19fcSEvan Bacon        },
176*26ad19fcSEvan Bacon        (error) => {
177*26ad19fcSEvan Bacon          this.updateStatus(type, error, null, null);
178*26ad19fcSEvan Bacon        }
179*26ad19fcSEvan Bacon      );
180*26ad19fcSEvan Bacon    }
181*26ad19fcSEvan Bacon  }
182*26ad19fcSEvan Bacon
183*26ad19fcSEvan Bacon  private updateStatus(
184*26ad19fcSEvan Bacon    type: StackType,
185*26ad19fcSEvan Bacon    error?: Error | null,
186*26ad19fcSEvan Bacon    stack?: Stack | null,
187*26ad19fcSEvan Bacon    codeFrame?: CodeFrame | null
188*26ad19fcSEvan Bacon  ): void {
189*26ad19fcSEvan Bacon    const lastStatus = this.symbolicated[type].status;
190*26ad19fcSEvan Bacon    if (error != null) {
191*26ad19fcSEvan Bacon      this.symbolicated[type] = {
192*26ad19fcSEvan Bacon        error,
193*26ad19fcSEvan Bacon        stack: null,
194*26ad19fcSEvan Bacon        status: 'FAILED',
195*26ad19fcSEvan Bacon      };
196*26ad19fcSEvan Bacon    } else if (stack != null) {
197*26ad19fcSEvan Bacon      if (codeFrame) {
198*26ad19fcSEvan Bacon        this.codeFrame = codeFrame;
199*26ad19fcSEvan Bacon      }
200*26ad19fcSEvan Bacon
201*26ad19fcSEvan Bacon      this.symbolicated[type] = {
202*26ad19fcSEvan Bacon        error: null,
203*26ad19fcSEvan Bacon        stack,
204*26ad19fcSEvan Bacon        status: 'COMPLETE',
205*26ad19fcSEvan Bacon      };
206*26ad19fcSEvan Bacon    } else {
207*26ad19fcSEvan Bacon      this.symbolicated[type] = {
208*26ad19fcSEvan Bacon        error: null,
209*26ad19fcSEvan Bacon        stack: null,
210*26ad19fcSEvan Bacon        status: 'PENDING',
211*26ad19fcSEvan Bacon      };
212*26ad19fcSEvan Bacon    }
213*26ad19fcSEvan Bacon
214*26ad19fcSEvan Bacon    const status = this.symbolicated[type].status;
215*26ad19fcSEvan Bacon    if (lastStatus !== status) {
216*26ad19fcSEvan Bacon      if (['COMPLETE', 'FAILED'].includes(status)) {
217*26ad19fcSEvan Bacon        this.flushCallbacks(type);
218*26ad19fcSEvan Bacon      }
219*26ad19fcSEvan Bacon    }
220*26ad19fcSEvan Bacon  }
221*26ad19fcSEvan Bacon}
222