addons/isl/src/operationsState.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {EnsureAssignedTogether} from 'shared/EnsureAssignedTogether';
b69ab319import type {Operation} from './operations/Operation';
b69ab3110import type {Disposable, Hash, ProgressStep, ServerToClientMessage} from './types';
b69ab3111
b69ab3112import {atom} from 'jotai';
b69ab3113import {useCallback} from 'react';
b69ab3114import {defer} from 'shared/utils';
b69ab3115import serverAPI from './ClientToServerAPI';
b69ab3116import {atomFamilyWeak, readAtom, writeAtom} from './jotaiUtils';
b69ab3117import {atomResetOnCwdChange} from './repositoryData';
4bb999b18import {applicationinfo} from './serverAPIState';
b69ab3119import {Timer} from './timer';
b69ab3120import {registerCleanup, registerDisposable, short} from './utils';
b69ab3121
b69ab3122export type OperationInfo = {
b69ab3123 operation: Operation;
b69ab3124 startTime?: Date;
b69ab3125 commandOutput?: Array<string>;
b69ab3126 currentProgress?: ProgressStep;
b69ab3127 /** progress message shown next to a commit */
b69ab3128 inlineProgress?: Map<Hash, string>;
b69ab3129 /** if true, we have sent "abort" request, the process might have exited or is going to exit soon */
b69ab3130 aborting?: boolean;
b69ab3131 /** if true, the operation process has exited AND there's no more optimistic commit state to show */
b69ab3132 hasCompletedOptimisticState?: boolean;
b69ab3133 /** if true, the operation process has exited AND there's no more optimistic changes to uncommitted changes to show */
b69ab3134 hasCompletedUncommittedChangesOptimisticState?: boolean;
b69ab3135 /** if true, the operation process has exited AND there's no more optimistic changes to merge conflicts to show */
b69ab3136 hasCompletedMergeConflictsOptimisticState?: boolean;
b69ab3137 warnings?: Array<string>;
b69ab3138} & EnsureAssignedTogether<{
b69ab3139 endTime: Date;
b69ab3140 exitCode: number;
b69ab3141}>;
b69ab3142
b69ab3143/**
b69ab3144 * The process has exited but exit code is unknown. Usually exit code is one byte.
b69ab3145 * '-1024' is unlikely to conflict with a valid exit code.
b69ab3146 */
b69ab3147export const EXIT_CODE_FORGET = -1024;
b69ab3148
b69ab3149/**
b69ab3150 * Bundle history of previous operations together with the current operation,
b69ab3151 * so we can easily manipulate operations together in one piece of state.
b69ab3152 */
b69ab3153export interface OperationList {
b69ab3154 /** The currently running operation, or the most recently run if not currently running. */
b69ab3155 currentOperation: OperationInfo | undefined;
b69ab3156 /** All previous operations oldest to newest, not including currentOperation */
b69ab3157 operationHistory: Array<OperationInfo>;
b69ab3158}
b69ab3159const defaultOperationList = () => ({currentOperation: undefined, operationHistory: []});
b69ab3160
b69ab3161function startNewOperation(newOperation: Operation, list: OperationList): OperationList {
b69ab3162 if (list.currentOperation?.operation.id === newOperation.id) {
b69ab3163 // we already have a new optimistic running operation, don't duplicate it
b69ab3164 return {...list};
b69ab3165 } else {
b69ab3166 // we need to start a new operation
b69ab3167 const operationHistory = [...list.operationHistory];
b69ab3168 if (list.currentOperation != null) {
b69ab3169 operationHistory.push(list.currentOperation);
b69ab3170 }
b69ab3171 const inlineProgress: Array<[string, string]> | undefined = newOperation
b69ab3172 .getInitialInlineProgress?.()
b69ab3173 ?.map(([k, v]) => [short(k), v]); // inline progress is keyed by short hashes, but let's do that conversion on behalf of operations.
b69ab3174 const currentOperation: OperationInfo = {
b69ab3175 operation: newOperation,
b69ab3176 startTime: new Date(),
b69ab3177 inlineProgress: inlineProgress == null ? undefined : new Map(inlineProgress),
b69ab3178 };
b69ab3179 return {...list, operationHistory, currentOperation};
b69ab3180 }
b69ab3181}
b69ab3182
b69ab3183/**
b69ab3184 * Ask the server if the current operation is still running.
b69ab3185 * The server might send back a "forgot" progress and we can mark
b69ab3186 * the operation as exited. This is useful when the operation exited
b69ab3187 * during disconnection.
b69ab3188 */
b69ab3189export function maybeRemoveForgottenOperation() {
b69ab3190 const list = readAtom(operationList);
b69ab3191 const operationId = list.currentOperation?.operation.id;
b69ab3192 if (operationId != null) {
b69ab3193 serverAPI.postMessage({
b69ab3194 type: 'requestMissedOperationProgress',
b69ab3195 operationId,
b69ab3196 });
b69ab3197 }
b69ab3198}
b69ab3199
b69ab31100export const operationList = atomResetOnCwdChange<OperationList>(defaultOperationList());
b69ab31101registerCleanup(
b69ab31102 operationList,
b69ab31103 serverAPI.onSetup(() => maybeRemoveForgottenOperation()),
b69ab31104 import.meta.hot,
b69ab31105);
b69ab31106registerDisposable(
b69ab31107 operationList,
b69ab31108 serverAPI.onMessageOfType('operationProgress', progress => {
b69ab31109 switch (progress.kind) {
b69ab31110 case 'spawn':
b69ab31111 writeAtom(operationList, list => {
b69ab31112 const operation = operationsById.get(progress.id);
b69ab31113 if (operation == null) {
b69ab31114 return list;
b69ab31115 }
b69ab31116
b69ab31117 return startNewOperation(operation, list);
b69ab31118 });
b69ab31119 break;
b69ab31120 case 'stdout':
b69ab31121 case 'stderr':
b69ab31122 writeAtom(operationList, current => {
b69ab31123 const currentOperation = current.currentOperation;
b69ab31124 if (currentOperation == null) {
b69ab31125 return current;
b69ab31126 }
b69ab31127
b69ab31128 return {
b69ab31129 ...current,
b69ab31130 currentOperation: {
b69ab31131 ...currentOperation,
b69ab31132 commandOutput: [...(currentOperation?.commandOutput ?? []), progress.message],
b69ab31133 currentProgress: undefined, // hide progress on new stdout, so it doesn't appear stuck
b69ab31134 },
b69ab31135 };
b69ab31136 });
b69ab31137 break;
b69ab31138 case 'inlineProgress':
b69ab31139 writeAtom(operationList, current => {
b69ab31140 const currentOperation = current.currentOperation;
b69ab31141 if (currentOperation == null) {
b69ab31142 return current;
b69ab31143 }
b69ab31144
b69ab31145 let inlineProgress: undefined | Map<string, string> =
b69ab31146 current.currentOperation?.inlineProgress ?? new Map();
b69ab31147 if (progress.hash) {
b69ab31148 if (progress.message) {
b69ab31149 inlineProgress.set(progress.hash, progress.message);
b69ab31150 } else {
b69ab31151 inlineProgress.delete(progress.hash);
b69ab31152 }
b69ab31153 } else {
b69ab31154 inlineProgress = undefined;
b69ab31155 }
b69ab31156
b69ab31157 const newCommandOutput = [...(currentOperation?.commandOutput ?? [])];
b69ab31158 if (progress.hash && progress.message) {
b69ab31159 // also add inline progress message as if it was on stdout,
b69ab31160 // so you can see it when reading back the final output
b69ab31161 newCommandOutput.push(`${progress.hash} - ${progress.message}\n`);
b69ab31162 }
b69ab31163
b69ab31164 return {
b69ab31165 ...current,
b69ab31166 currentOperation: {
b69ab31167 ...currentOperation,
b69ab31168 inlineProgress,
b69ab31169 },
b69ab31170 };
b69ab31171 });
b69ab31172 break;
b69ab31173 case 'progress':
b69ab31174 writeAtom(operationList, current => {
b69ab31175 const currentOperation = current.currentOperation;
b69ab31176 if (currentOperation == null) {
b69ab31177 return current;
b69ab31178 }
b69ab31179
b69ab31180 const newCommandOutput = [...(currentOperation?.commandOutput ?? [])];
b69ab31181 if (newCommandOutput.at(-1)?.trim() !== progress.progress.message) {
b69ab31182 // also add progress message as if it was on stdout,
b69ab31183 // so you can see it when reading back the final output,
b69ab31184 // but only if it's a different progress message than we've seen.
b69ab31185 newCommandOutput.push(progress.progress.message + '\n');
b69ab31186 }
b69ab31187
b69ab31188 return {
b69ab31189 ...current,
b69ab31190 currentOperation: {
b69ab31191 ...currentOperation,
b69ab31192 commandOutput: newCommandOutput,
b69ab31193 currentProgress: progress.progress,
b69ab31194 },
b69ab31195 };
b69ab31196 });
b69ab31197 break;
b69ab31198 case 'warning':
b69ab31199 writeAtom(operationList, current => {
b69ab31200 const currentOperation = current.currentOperation;
b69ab31201 if (currentOperation == null) {
b69ab31202 return current;
b69ab31203 }
b69ab31204 const warnings = [...(currentOperation?.warnings ?? []), progress.warning];
b69ab31205 return {
b69ab31206 ...current,
b69ab31207 currentOperation: {
b69ab31208 ...currentOperation,
b69ab31209 warnings,
b69ab31210 },
b69ab31211 };
b69ab31212 });
b69ab31213 break;
b69ab31214 case 'exit':
b69ab31215 case 'forgot':
b69ab31216 writeAtom(operationList, current => {
b69ab31217 const currentOperation = current.currentOperation;
b69ab31218
b69ab31219 let operationThatExited: OperationInfo | undefined;
b69ab31220
b69ab31221 if (
b69ab31222 currentOperation == null ||
b69ab31223 currentOperation.exitCode != null ||
b69ab31224 currentOperation.operation.id !== progress.id
b69ab31225 ) {
b69ab31226 // We've seen cases where we somehow got this exit out of order.
b69ab31227 // Instead of updating the currentOperation, we need to find the matching historical operation.
b69ab31228 // (which has the matching ID, and as long as it hasn't already been marked as exited)
b69ab31229
b69ab31230 operationThatExited = current.operationHistory.find(
b69ab31231 op => op.operation.id === progress.id && op.exitCode == null,
b69ab31232 );
b69ab31233
b69ab31234 window.globalIslClientTracker.track('ExitMessageOutOfOrder', {
b69ab31235 extras: {
b69ab31236 operationThatExited: operationThatExited?.operation.trackEventName,
b69ab31237 },
b69ab31238 });
b69ab31239 }
b69ab31240
b69ab31241 if (operationThatExited == null) {
b69ab31242 operationThatExited = currentOperation;
b69ab31243 }
b69ab31244
b69ab31245 if (operationThatExited == null) {
b69ab31246 // We can't do anything about this.
b69ab31247 return current;
b69ab31248 }
b69ab31249
b69ab31250 const {exitCode, timestamp} =
b69ab31251 progress.kind === 'exit'
b69ab31252 ? progress
b69ab31253 : {exitCode: EXIT_CODE_FORGET, timestamp: Date.now()};
b69ab31254 const complete = operationCompletionCallbacks.get(operationThatExited.operation.id);
b69ab31255 complete?.(
b69ab31256 exitCode === 0 ? undefined : new Error(`Process exited with code ${exitCode}`),
b69ab31257 );
b69ab31258 operationCompletionCallbacks.delete(operationThatExited.operation.id);
b69ab31259
b69ab31260 const updatedOperation = {
b69ab31261 ...operationThatExited,
b69ab31262 exitCode,
b69ab31263 endTime: new Date(timestamp),
b69ab31264 inlineProgress: undefined, // inline progress never lasts after exiting
b69ab31265 };
b69ab31266
b69ab31267 if (operationThatExited === currentOperation) {
b69ab31268 return {
b69ab31269 ...current,
b69ab31270 currentOperation: updatedOperation,
b69ab31271 };
b69ab31272 } else {
b69ab31273 return {
b69ab31274 ...current,
b69ab31275 operationHistory: current.operationHistory.map(op => {
b69ab31276 if (op === operationThatExited) {
b69ab31277 return updatedOperation;
b69ab31278 }
b69ab31279 return op;
b69ab31280 }),
b69ab31281 };
b69ab31282 }
b69ab31283 });
b69ab31284 break;
b69ab31285 }
b69ab31286 }),
b69ab31287 import.meta.hot,
b69ab31288);
b69ab31289
b69ab31290/** If an operation in the queue fails, it will remove all further queued operations.
b69ab31291 * On such an error, we move the remaining operations into this separate state to be shown in the UI as a warning.
b69ab31292 * This lets you see and understand what actions you took that were "reverted", so you might recreate those steps. */
b69ab31293export const queuedOperationsErrorAtom = atomResetOnCwdChange<
b69ab31294 | {error: Error; operationThatErrored: Operation | undefined; operations: Array<Operation>}
b69ab31295 | undefined
b69ab31296>(undefined);
b69ab31297
b69ab31298export const inlineProgressByHash = atomFamilyWeak((hash: Hash) =>
b69ab31299 atom(get => {
b69ab31300 const info = get(operationList);
b69ab31301 const inlineProgress = info.currentOperation?.inlineProgress;
b69ab31302 if (inlineProgress == null) {
b69ab31303 return undefined;
b69ab31304 }
b69ab31305 const shortHash = short(hash); // progress messages come indexed by short hash
b69ab31306 return inlineProgress.get(shortHash);
b69ab31307 }),
b69ab31308);
b69ab31309
b69ab31310export const operationBeingPreviewed = atomResetOnCwdChange<Operation | undefined>(undefined);
b69ab31311
b69ab31312/** We don't send entire operations to the server, since not all fields are serializable.
b69ab31313 * Thus, when the server tells us about the queue of operations, we need to know which operation it's talking about.
b69ab31314 * Store recently run operations by id. Add to this map whenever a new operation is run. Remove when an operation process exits (successfully or unsuccessfully)
b69ab31315 */
b69ab31316const operationsById = new Map<string, Operation>();
b69ab31317/** Store callbacks to run when an operation completes. This is stored outside of the operation since Operations are typically Immutable. */
b69ab31318const operationCompletionCallbacks = new Map<string, (error?: Error) => void>();
b69ab31319
b69ab31320/**
b69ab31321 * Subscribe to an operation exiting. Useful for handling cases where an operation fails
b69ab31322 * and it should reset the UI to try again.
b69ab31323 */
b69ab31324export function onOperationExited(
b69ab31325 cb: (
b69ab31326 message: ServerToClientMessage & {type: 'operationProgress'; kind: 'exit'},
b69ab31327 operation: Operation,
b69ab31328 ) => unknown,
b69ab31329): Disposable {
b69ab31330 return serverAPI.onMessageOfType('operationProgress', progress => {
b69ab31331 if (progress.kind === 'exit') {
b69ab31332 const op = operationsById.get(progress.id);
b69ab31333 if (op) {
b69ab31334 cb(progress, op);
b69ab31335 }
b69ab31336 }
b69ab31337 });
b69ab31338}
b69ab31339
b69ab31340/**
b69ab31341 * If no operations are running or queued, returns undefined.
b69ab31342 * If something is running or queued, return a Promise that resolves
b69ab31343 * when there's no operation running and nothing remains queued (the UI is "idle")
b69ab31344 * Does not wait for optimistic state to be resolved, only for commands to finish.
b69ab31345 */
b69ab31346export function waitForNothingRunning(): Promise<void> | undefined {
b69ab31347 const currentOperation = readAtom(operationList).currentOperation;
b69ab31348 const somethingRunning = currentOperation != null && currentOperation?.exitCode == null;
b69ab31349 const anythingQueued = readAtom(queuedOperations).length > 0;
b69ab31350 if (!somethingRunning && !anythingQueued) {
b69ab31351 // nothing running, nothing queued -> return undefined immediately
b69ab31352 return undefined;
b69ab31353 }
b69ab31354 return serverAPI
b69ab31355 .nextMessageMatching(
b69ab31356 'operationProgress',
b69ab31357 // something running but nothing queued -> resolve when the operation exits
b69ab31358 // something queued -> resolve when the next operation exits, but only once the queue is empty
b69ab31359 // something running but exits non-zero -> everything queue'd will be cancelled anyway, resolve immediately
b69ab31360 msg => msg.kind === 'exit' && (msg.exitCode !== 0 || readAtom(queuedOperations).length === 0),
b69ab31361 )
b69ab31362 .then(() => undefined);
b69ab31363}
b69ab31364
b69ab31365export const queuedOperations = atomResetOnCwdChange<Array<Operation>>([]);
b69ab31366registerDisposable(
b69ab31367 queuedOperations,
b69ab31368 serverAPI.onMessageOfType('operationProgress', progress => {
b69ab31369 switch (progress.kind) {
b69ab31370 case 'queue':
b69ab31371 case 'spawn': // spawning doubles as our notification to dequeue the next operation, and includes the new queue state.
b69ab31372 // Update with the latest queue state. We expect this to be sent whenever we try to run a command but it gets queued.
b69ab31373 writeAtom(queuedOperations, () => {
b69ab31374 return progress.queue
b69ab31375 .map(opId => operationsById.get(opId))
b69ab31376 .filter((op): op is Operation => op != null);
b69ab31377 });
b69ab31378 // On spawn, we can clear the queued commands error. The error would have already been shown and then further acted on.
b69ab31379 // This wouldn't happen automatically, so we consider this an explicit user acknowledgement.
b69ab31380 // This also means this error state and the queuedOperations state should be mutually exclusive.
b69ab31381 writeAtom(queuedOperationsErrorAtom, undefined);
b69ab31382 break;
b69ab31383 case 'error': {
b69ab31384 saveQueuedOperationsOnError(progress.id, new Error(progress.error));
b69ab31385
b69ab31386 writeAtom(queuedOperations, []); // empty queue when a command hits an error
b69ab31387 break;
b69ab31388 }
b69ab31389 case 'exit': {
b69ab31390 setTimeout(() => {
b69ab31391 // we don't need to care about this operation anymore after this tick,
b69ab31392 // once all other callsites processing 'operationProgress' messages have run.
b69ab31393 operationsById.delete(progress.id);
b69ab31394 });
b69ab31395 if (progress.exitCode != null && progress.exitCode !== 0) {
b69ab31396 saveQueuedOperationsOnError(progress.id, new Error('command exited with non-zero code'));
b69ab31397
b69ab31398 // if any process in the queue exits with an error, the entire queue is cleared.
b69ab31399 writeAtom(queuedOperations, []);
b69ab31400 }
b69ab31401 break;
b69ab31402 }
b69ab31403 }
b69ab31404 }),
b69ab31405 import.meta.hot,
b69ab31406);
b69ab31407
b69ab31408function saveQueuedOperationsOnError(operationIdThatErrored: string, error: Error) {
b69ab31409 const queued = readAtom(queuedOperations);
b69ab31410 // This may be called twice for the same operation (error, then also exit).
b69ab31411 // Don't clear the error state if it's for the same operation, even if the queue is now empty.
b69ab31412 if (readAtom(queuedOperationsErrorAtom)?.operationThatErrored?.id !== operationIdThatErrored) {
b69ab31413 writeAtom(
b69ab31414 queuedOperationsErrorAtom,
b69ab31415 queued.length === 0
b69ab31416 ? undefined // invariant: queuedOperationsError.operations should never be [], rather the whole thing is undefined
b69ab31417 : {
b69ab31418 operationThatErrored: operationsById.get(operationIdThatErrored),
b69ab31419 error,
b69ab31420 operations: readAtom(queuedOperations),
b69ab31421 },
b69ab31422 );
b69ab31423 }
b69ab31424}
b69ab31425
b69ab31426export function getLatestOperationInfo(operation: Operation): OperationInfo | undefined {
b69ab31427 const list = readAtom(operationList);
b69ab31428 const info =
b69ab31429 list.currentOperation?.operation === operation
b69ab31430 ? list.currentOperation
b69ab31431 : list.operationHistory.find(op => op.operation === operation);
b69ab31432
b69ab31433 return info;
b69ab31434}
b69ab31435
b69ab31436function runOperationImpl(operation: Operation): Promise<undefined | Error> {
b69ab31437 // TODO: check for hashes in arguments that are known to be obsolete already,
b69ab31438 // and mark those to not be rewritten.
b69ab31439 serverAPI.postMessage({
b69ab31440 type: 'runOperation',
b69ab31441 operation: operation.getRunnableOperation(),
b69ab31442 });
b69ab31443 const deferred = defer<undefined | Error>();
b69ab31444 operationCompletionCallbacks.set(operation.id, (err?: Error) => {
b69ab31445 deferred.resolve(err);
b69ab31446 });
b69ab31447
b69ab31448 operationsById.set(operation.id, operation);
b69ab31449 const ongoing = readAtom(operationList);
b69ab31450
b69ab31451 if (ongoing?.currentOperation != null && ongoing.currentOperation.exitCode == null) {
b69ab31452 // Add to the queue optimistically. The server will tell us the real state of the queue when it gets our run request.
b69ab31453 writeAtom(queuedOperations, prev => [...(prev || []), operation]);
b69ab31454 } else {
b69ab31455 // start a new operation. We need to manage the previous operations
b69ab31456 writeAtom(operationList, list => startNewOperation(operation, list));
b69ab31457 }
b69ab31458
b69ab31459 // Check periodically with the server that the process is still running.
b69ab31460 // This is a fallback in case the server cannot send us "exit" messages.
b69ab31461 // This timer will auto disable when currentOperation becomes null.
b69ab31462 currentOperationHeartbeatTimer.enabled = true;
b69ab31463
b69ab31464 return deferred.promise;
b69ab31465}
b69ab31466
b69ab31467const currentOperationHeartbeatTimer = new Timer(() => {
b69ab31468 const currentOp = readAtom(operationList).currentOperation;
b69ab31469 if (currentOp == null || currentOp.endTime != null) {
b69ab31470 // Stop the timer.
b69ab31471 return false;
b69ab31472 }
b69ab31473 maybeRemoveForgottenOperation();
b69ab31474}, 5000);
b69ab31475
b69ab31476/**
b69ab31477 * Returns callback to run an operation.
b69ab31478 * Will be queued by the server if other operations are already running.
b69ab31479 * This returns a promise that resolves when this operation has exited
b69ab31480 * (though its optimistic state may not have finished resolving yet).
b69ab31481 * Note: Most callsites won't await this promise, and just use queueing. If you do, you should probably use `throwOnError = true` to detect errors.
b69ab31482 * TODO: should we refactor this into a separate function if you want to await the result, which always throws?
b69ab31483 * Note: There's no need to wait for this promise to resolve before starting another operation,
b69ab31484 * successive operations will queue up with a nicer UX than if you awaited each one.
b69ab31485 */
b69ab31486export function useRunOperation() {
b69ab31487 return useCallback(async (operation: Operation, throwOnError?: boolean): Promise<void> => {
4bb999b488 if (readAtom(applicationinfo)?.readOnly) {
4bb999b489 return;
4bb999b490 }
b69ab31491 const result = await runOperationImpl(operation);
b69ab31492 if (result != null && throwOnError) {
b69ab31493 throw result;
b69ab31494 }
b69ab31495 }, []);
b69ab31496}
b69ab31497
b69ab31498/**
b69ab31499 * Returns callback to abort the running operation.
b69ab31500 */
b69ab31501export function useAbortRunningOperation() {
b69ab31502 return useCallback((operationId: string) => {
b69ab31503 serverAPI.postMessage({
b69ab31504 type: 'abortRunningOperation',
b69ab31505 operationId,
b69ab31506 });
b69ab31507 const ongoing = readAtom(operationList);
b69ab31508 if (ongoing?.currentOperation?.operation?.id === operationId) {
b69ab31509 // Mark 'aborting' as true.
b69ab31510 writeAtom(operationList, list => {
b69ab31511 const currentOperation = list.currentOperation;
b69ab31512 if (currentOperation != null) {
b69ab31513 return {...list, currentOperation: {aborting: true, ...currentOperation}};
b69ab31514 }
b69ab31515 return list;
b69ab31516 });
b69ab31517 }
b69ab31518 }, []);
b69ab31519}
b69ab31520
b69ab31521/**
b69ab31522 * Returns callback to run the operation currently being previewed, or cancel the preview.
b69ab31523 * Set operationBeingPreviewed to start a preview.
b69ab31524 */
b69ab31525export function useRunPreviewedOperation() {
b69ab31526 return useCallback((isCancel: boolean, operation?: Operation) => {
b69ab31527 if (isCancel) {
b69ab31528 writeAtom(operationBeingPreviewed, undefined);
b69ab31529 return;
b69ab31530 }
b69ab31531
b69ab31532 const operationToRun = operation ?? readAtom(operationBeingPreviewed);
b69ab31533 writeAtom(operationBeingPreviewed, undefined);
b69ab31534 if (operationToRun) {
b69ab31535 runOperationImpl(operationToRun);
b69ab31536 }
b69ab31537 }, []);
b69ab31538}
b69ab31539
b69ab31540/**
b69ab31541 * It's possible for optimistic state to be incorrect, e.g. if some assumption about a command is incorrect in an edge case
b69ab31542 * but the command doesn't exit non-zero. This provides a backdoor to clear out all ongoing optimistic state from *previous* commands.
b69ab31543 * Queued commands and the currently running command will not be affected.
b69ab31544 */
b69ab31545export function useClearAllOptimisticState() {
b69ab31546 return useCallback(() => {
b69ab31547 writeAtom(operationList, list => {
b69ab31548 const operationHistory = [...list.operationHistory];
b69ab31549 for (let i = 0; i < operationHistory.length; i++) {
b69ab31550 if (operationHistory[i].exitCode != null) {
b69ab31551 if (!operationHistory[i].hasCompletedOptimisticState) {
b69ab31552 operationHistory[i] = {...operationHistory[i], hasCompletedOptimisticState: true};
b69ab31553 }
b69ab31554 if (!operationHistory[i].hasCompletedUncommittedChangesOptimisticState) {
b69ab31555 operationHistory[i] = {
b69ab31556 ...operationHistory[i],
b69ab31557 hasCompletedUncommittedChangesOptimisticState: true,
b69ab31558 };
b69ab31559 }
b69ab31560 if (!operationHistory[i].hasCompletedMergeConflictsOptimisticState) {
b69ab31561 operationHistory[i] = {
b69ab31562 ...operationHistory[i],
b69ab31563 hasCompletedMergeConflictsOptimisticState: true,
b69ab31564 };
b69ab31565 }
b69ab31566 }
b69ab31567 }
b69ab31568 const currentOperation =
b69ab31569 list.currentOperation == null ? undefined : {...list.currentOperation};
b69ab31570 if (currentOperation?.exitCode != null) {
b69ab31571 currentOperation.hasCompletedOptimisticState = true;
b69ab31572 currentOperation.hasCompletedUncommittedChangesOptimisticState = true;
b69ab31573 currentOperation.hasCompletedMergeConflictsOptimisticState = true;
b69ab31574 }
b69ab31575 return {currentOperation, operationHistory};
b69ab31576 });
b69ab31577 }, []);
b69ab31578}