4.9 KB158 lines
Blame
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
8import type {Atom} from 'jotai';
9import type {Json} from 'shared/typeUtils';
10import type {AtomFamilyWeak} from '../jotaiUtils';
11
12import {SelfUpdate} from 'shared/immutableExt';
13import {editedCommitMessages} from '../CommitInfoView/CommitInfoState';
14import {latestSuccessorsMapAtom} from '../SuccessionTracker';
15import {allDiffSummaries, codeReviewProvider} from '../codeReview/CodeReviewInfo';
16import {readAtom} from '../jotaiUtils';
17import {operationBeingPreviewed, operationList, queuedOperations} from '../operationsState';
18import {uncommittedSelection} from '../partialSelection';
19import {dagWithPreviews} from '../previews';
20import {selectedCommits} from '../selection';
21import {
22 latestCommitsData,
23 latestUncommittedChangesData,
24 mergeConflicts,
25 repositoryData,
26 submodulesByRoot,
27} from '../serverAPIState';
28
29export type UIStateSnapshot = {[key: string]: Json};
30export type AtomsState = {[key: string]: unknown};
31
32type AtomOrFamily = Atom<unknown> | AtomFamilyWeak<string, Atom<unknown>>;
33
34function listInterestingAtoms(): Array<AtomOrFamily> {
35 return [
36 allDiffSummaries,
37 codeReviewProvider,
38 repositoryData,
39 latestCommitsData,
40 latestSuccessorsMapAtom,
41 latestUncommittedChangesData,
42 dagWithPreviews,
43 mergeConflicts,
44 operationBeingPreviewed,
45 operationList,
46 queuedOperations,
47 selectedCommits,
48 uncommittedSelection,
49 // These are atomFamilies.
50 editedCommitMessages,
51 submodulesByRoot,
52 ];
53}
54
55/** Read all "interesting" atoms and returns a single object that contains them all. */
56export function readInterestingAtoms(): AtomsState {
57 return Object.fromEntries(
58 listInterestingAtoms().map(a => [a.debugLabel ?? a.toString(), readAtomOrFamily(a)]),
59 );
60}
61
62/** Try to serialize the `state` so they can be represented in plain JSON. */
63export function serializeAtomsState(state: AtomsState): UIStateSnapshot {
64 const newEntries = Object.entries(state).map(([key, value]) => {
65 return [key, serialize(value as Serializable)];
66 });
67 return Object.fromEntries(newEntries);
68}
69
70function readAtomOrFamily(atomOrFamily: AtomOrFamily): unknown {
71 if (typeof atomOrFamily === 'function') {
72 // atomFamily. Read its values from weakCache.
73 const result = new Map<string, unknown>();
74 for (const [key, weak] of atomOrFamily.weakCache.entries()) {
75 const value = weak.deref();
76 result.set(key, value === undefined ? undefined : readAtom(value));
77 }
78 return result;
79 } else {
80 return readAtom(atomOrFamily);
81 }
82}
83
84type Serializable = Json | {toJSON: () => Serializable};
85
86function serialize(initialArg: Serializable): Json {
87 let arg = initialArg;
88
89 const isObject = arg != null && typeof arg === 'object';
90
91 // Extract debug state provided by the object. This applies to both immutable and regular objects.
92 // This needs to happen before unwrapping SelfUpdate.
93 let debugState = null;
94 if (isObject) {
95 // If the object defines `getDebugState`. Call it to get more (easier to visualize) states.
96 const maybeGetDebugState = (arg as {getDebugState?: () => {[key: string]: Json}}).getDebugState;
97 if (maybeGetDebugState != null) {
98 debugState = maybeGetDebugState.call(arg);
99 }
100 }
101
102 // Unwrap SelfUpdate types.
103 if (arg instanceof SelfUpdate) {
104 arg = arg.inner;
105 }
106
107 // Convert known immutable types.
108 if (arg != null && typeof arg === 'object') {
109 const maybeToJSON = (arg as {toJSON?: () => Json}).toJSON;
110 if (maybeToJSON !== undefined) {
111 arg = maybeToJSON.call(arg);
112 if (typeof arg === 'object' && debugState != null) {
113 arg = {...debugState, ...arg};
114 }
115 }
116 }
117
118 if (arg === undefined) {
119 return null;
120 }
121
122 if (
123 typeof arg === 'number' ||
124 typeof arg === 'boolean' ||
125 typeof arg === 'string' ||
126 arg === null
127 ) {
128 return arg;
129 }
130
131 if (arg instanceof Map) {
132 return Array.from(arg.entries()).map(([key, val]) => [serialize(key), serialize(val)]);
133 } else if (arg instanceof Set) {
134 return Array.from(arg.values()).map(serialize);
135 } else if (arg instanceof Error) {
136 return {message: arg.message ?? null, stack: arg.stack ?? null};
137 } else if (arg instanceof Date) {
138 return `Date: ${arg.valueOf()}`;
139 } else if (Array.isArray(arg)) {
140 return arg.map(a => serialize(a));
141 } else if (typeof arg === 'object') {
142 const newObj: Json = debugState ?? {};
143 for (const [propertyName, propertyValue] of Object.entries(arg)) {
144 // Skip functions.
145 if (typeof propertyValue === 'function') {
146 continue;
147 }
148 newObj[propertyName] = serialize(propertyValue);
149 }
150
151 return newObj;
152 }
153
154 // Return a dummy value instead of throw so if an item in a container is "bad",
155 // it does not turn the whole container into an error.
156 return `<unserializable: ${arg}>`;
157}
158