| 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 | |
| 8 | import type {Atom} from 'jotai'; |
| 9 | import type {Json} from 'shared/typeUtils'; |
| 10 | import type {AtomFamilyWeak} from '../jotaiUtils'; |
| 11 | |
| 12 | import {SelfUpdate} from 'shared/immutableExt'; |
| 13 | import {editedCommitMessages} from '../CommitInfoView/CommitInfoState'; |
| 14 | import {latestSuccessorsMapAtom} from '../SuccessionTracker'; |
| 15 | import {allDiffSummaries, codeReviewProvider} from '../codeReview/CodeReviewInfo'; |
| 16 | import {readAtom} from '../jotaiUtils'; |
| 17 | import {operationBeingPreviewed, operationList, queuedOperations} from '../operationsState'; |
| 18 | import {uncommittedSelection} from '../partialSelection'; |
| 19 | import {dagWithPreviews} from '../previews'; |
| 20 | import {selectedCommits} from '../selection'; |
| 21 | import { |
| 22 | latestCommitsData, |
| 23 | latestUncommittedChangesData, |
| 24 | mergeConflicts, |
| 25 | repositoryData, |
| 26 | submodulesByRoot, |
| 27 | } from '../serverAPIState'; |
| 28 | |
| 29 | export type UIStateSnapshot = {[key: string]: Json}; |
| 30 | export type AtomsState = {[key: string]: unknown}; |
| 31 | |
| 32 | type AtomOrFamily = Atom<unknown> | AtomFamilyWeak<string, Atom<unknown>>; |
| 33 | |
| 34 | function 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. */ |
| 56 | export 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. */ |
| 63 | export 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 | |
| 70 | function 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 | |
| 84 | type Serializable = Json | {toJSON: () => Serializable}; |
| 85 | |
| 86 | function 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 | |