main
js 139 lines 3.84 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 Thenable,
12 FulfilledThenable,
13 RejectedThenable,
14 } from 'shared/ReactTypes';
15 import type {GitHubIssue} from './githubAPI';
16
17 import * as React from 'react';
18
19 import {unstable_getCacheForType as getCacheForType} from 'react';
20 import {searchGitHubIssues} from './githubAPI';
21
22 const API_TIMEOUT = 3000;
23 function readRecord<T>(record: Thenable<T>): T | null {
24 if (typeof React.use === 'function') {
25 try {
26 // eslint-disable-next-line react-hooks-published/rules-of-hooks
27 return React.use(record);
28 } catch (x) {
29 if (x === null) {
30 return null;
31 }
32 throw x;
33 }
34 }
35 if (record.status === 'fulfilled') {
36 return record.value;
37 } else if (record.status === 'rejected') {
38 return null;
39 } else {
40 throw record;
41 }
42 }
43
44 type GitHubIssueMap = Map<string, Thenable<GitHubIssue>>;
45
46 function createMap(): GitHubIssueMap {
47 return new Map();
48 }
49
50 function getRecordMap(): Map<string, Thenable<GitHubIssue>> {
51 return getCacheForType(createMap);
52 }
53
54 export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
55 errorMessage = normalizeErrorMessage(errorMessage);
56
57 const map = getRecordMap();
58 let record = map.get(errorMessage);
59
60 if (!record) {
61 const callbacks = new Set<(value: any) => mixed>();
62 const rejectCallbacks = new Set<(reason: mixed) => mixed>();
63 const thenable: Thenable<GitHubIssue> = {
64 status: 'pending',
65 value: null,
66 reason: null,
67 then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
68 callbacks.add(callback);
69 rejectCallbacks.add(reject);
70 },
71
72 // Optional property, read by React to name this I/O in async debug info:
73 displayName: `Searching GitHub issues for error "${errorMessage}"`,
74 };
75 const wake = () => {
76 // This assumes they won't throw.
77 callbacks.forEach(callback => callback((thenable as any).value));
78 callbacks.clear();
79 rejectCallbacks.clear();
80 };
81 const wakeRejections = () => {
82 // This assumes they won't throw.
83 rejectCallbacks.forEach(callback => callback((thenable as any).reason));
84 rejectCallbacks.clear();
85 callbacks.clear();
86 };
87 record = thenable;
88
89 let didTimeout = false;
90
91 searchGitHubIssues(errorMessage)
92 .then(maybeItem => {
93 if (didTimeout) {
94 return;
95 }
96
97 if (maybeItem) {
98 const fulfilledThenable: FulfilledThenable<GitHubIssue> =
99 thenable as any;
100 fulfilledThenable.status = 'fulfilled';
101 fulfilledThenable.value = maybeItem;
102 wake();
103 } else {
104 const notFoundThenable: RejectedThenable<GitHubIssue> =
105 thenable as any;
106 notFoundThenable.status = 'rejected';
107 notFoundThenable.reason = null;
108 wakeRejections();
109 }
110 })
111 .catch(error => {
112 const rejectedThenable: RejectedThenable<GitHubIssue> = thenable as any;
113 rejectedThenable.status = 'rejected';
114 rejectedThenable.reason = null;
115 wakeRejections();
116 });
117
118 // Only wait a little while for GitHub results before showing a fallback.
119 setTimeout(() => {
120 didTimeout = true;
121
122 const timedoutThenable: RejectedThenable<GitHubIssue> = thenable as any;
123 timedoutThenable.status = 'rejected';
124 timedoutThenable.reason = null;
125 wakeRejections();
126 }, API_TIMEOUT);
127
128 map.set(errorMessage, record);
129 }
130
131 const response = readRecord(record);
132 return response;
133 }
134
135 function normalizeErrorMessage(errorMessage: string): string {
136 // Remove Fiber IDs from error message (as those will be unique).
137 errorMessage = errorMessage.replace(/"[0-9]+"/, '');
138 return errorMessage;
139 }