Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update mobxpromise and fix circular dependency caused by onResult #4836

Merged
merged 11 commits into from
Jan 29, 2024
18 changes: 18 additions & 0 deletions end-to-end-test/remote/specs/core/results.logic.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ describe('case set selection in modify query form', function() {
});
});

describe('gene list input', function() {
beforeEach(function() {
var url = `${CBIOPORTAL_URL}/index.do?cancer_study_id=coadread_tcga_pub&Z_SCORE_THRESHOLD=2&RPPA_SCORE_THRESHOLD=2&data_priority=0&case_set_id=coadread_tcga_pub_rppa&gene_list=KRAS%2520NRAS%2520BRAF&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic`;
goToUrlAndSetLocalStorage(url);
$('#modifyQueryBtn').waitForExist({ timeout: 60000 });
});

// we're testing this because it was broken
it('allows gene textarea update', () => {
$('#modifyQueryBtn').click();
const textarea = getElementByTestHandle('geneSet');
textarea.waitForDisplayed();
textarea.setValue('TP53 BRAF');

assert(textarea.getValue() === 'TP53 BRAF');
});
});

describe('genetic profile selection in modify query form', function() {
beforeEach(function() {
var url = `${CBIOPORTAL_URL}/index.do?cancer_study_id=chol_tcga&Z_SCORE_THRESHOLD=2.0&RPPA_SCORE_THRESHOLD=2.0&data_priority=0&case_set_id=chol_tcga_all&gene_list=EGFR&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=chol_tcga_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=chol_tcga_gistic&genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION=chol_tcga_rppa_Zscores`;
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@
"mobx-react-lite": "3.0.1",
"mobx-react-router": "4.1.0",
"mobx-utils": "6.0.1",
"mobxpromise": "github:cbioportal/mobxpromise#c3429672eb39be54e54ce14a8636e8d843729db3",
"numeral": "^2.0.6",
"object-sizeof": "^1.2.0",
"oncokb-frontend-commons": "^0.0.21",
Expand Down
1 change: 0 additions & 1 deletion packages/cbioportal-frontend-commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"jquery": "^3.2.1",
"lodash": "^4.17.15",
"measure-text": "0.0.4",
"mobxpromise": "github:cbioportal/mobxpromise#303db72588860bff0a6862a4f07a4e8a3578c94f",
"numeral": "^2.0.6",
"object-sizeof": "^1.2.0",
"oncokb-ts-api-client": "^1.3.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/cbioportal-frontend-commons/src/api/remoteData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
MobxPromise,
MobxPromiseFactory,
MobxPromiseInputUnion,
hasObservers,
} from 'mobxpromise';
} from '../lib/MobxPromise';
import { hasObservers } from '../lib/mobxPromiseUtils';

type errorHandler = (error: Error) => void;

Expand Down
2 changes: 2 additions & 0 deletions packages/cbioportal-frontend-commons/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export * from './lib/TextTruncationUtils';
export * from './lib/urls';
export * from './lib/webdriverUtils';
export * from './lib/TickUtils';
export * from './lib/MobxPromise';
export * from './lib/mobxPromiseUtils';

export { default as CBIOPORTAL_VICTORY_THEME } from './theme/cBioPortalTheme';
export * from './theme/cBioPortalTheme';
Expand Down
305 changes: 305 additions & 0 deletions packages/cbioportal-frontend-commons/src/lib/MobxPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { observable, action, computed, makeObservable } from 'mobx';

/**
* This tagged union type describes the interoperability of MobxPromise properties.
*/
type MobxPromiseStatus = 'pending' | 'error' | 'complete';
export type MobxPromiseUnionType<R> = (
| {
status: 'pending';
isPending: true;
isError: false;
isComplete: false;
result: R | undefined;
error: Error | undefined;
}
| {
status: 'error';
isPending: false;
isError: true;
isComplete: false;
result: R | undefined;
error: Error;
}
| {
status: 'complete';
isPending: false;
isError: false;
isComplete: true;
result: R;
error: Error | undefined;
}
) & { peekStatus: MobxPromiseStatus };
export type MobxPromiseUnionTypeWithDefault<R> = (
| {
status: 'pending';
isPending: true;
isError: false;
isComplete: false;
result: R;
error: Error | undefined;
}
| {
status: 'error';
isPending: false;
isError: true;
isComplete: false;
result: R;
error: Error;
}
| {
status: 'complete';
isPending: false;
isError: false;
isComplete: true;
result: R;
error: Error | undefined;
}
) & { peekStatus: MobxPromiseStatus };

export type MobxPromiseInputUnion<R> =
| PromiseLike<R>
| (() => PromiseLike<R>)
| MobxPromiseInputParams<R>;
export type MobxPromiseInputParams<R> = {
/**
* A function that returns a list of MobxPromise objects which are dependencies of the invoke function.
*/
await?: MobxPromise_await;

/**
* A function that returns the async result or a promise for the async result.
*/
invoke: MobxPromise_invoke<R>;

/**
* Default result in place of undefined
*/
default?: R;

/**
* A function that will be called when the latest promise from invoke() is resolved.
* It will not be called for out-of-date promises.
*/
onResult?: (result?: R) => void;

/**
* A function that will be called when the latest promise from invoke() is rejected.
* It will not be called for out-of-date promises.
*/
onError?: (error: Error) => void;
};
export type MobxPromise_await = () => Array<
| MobxPromiseUnionTypeWithDefault<any>
| MobxPromiseUnionType<any>
| MobxPromise<any>
>;
export type MobxPromise_invoke<R> = () => PromiseLike<R>;
export type MobxPromiseInputParamsWithDefault<R> = {
await?: MobxPromise_await;
invoke: MobxPromise_invoke<R>;
default: R;
onResult?: (result: R) => void;
onError?: (error: Error) => void;
};

/**
* MobxPromise provides an observable interface for a computed promise.
* @author adufilie http://github.com/adufilie
*/
export class MobxPromiseImpl<R> {
static isPromiseLike(value: any) {
return (
value != null &&
typeof value === 'object' &&
typeof value.then === 'function'
);
}

static normalizeInput<R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseInputParamsWithDefault<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>,
defaultResult?: R
): MobxPromiseInputParamsWithDefault<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>
): MobxPromiseInputParams<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>,
defaultResult?: R
) {
if (typeof input === 'function')
return { invoke: input, default: defaultResult };

if (MobxPromiseImpl.isPromiseLike(input))
return {
invoke: () => input as PromiseLike<R>,
default: defaultResult,
};

input = input as MobxPromiseInputParams<R>;
if (defaultResult !== undefined)
input = { ...input, default: defaultResult };
return input;
}

constructor(input: MobxPromiseInputUnion<R>, defaultResult?: R) {
makeObservable<MobxPromiseImpl<R>>(this);

let norm = MobxPromiseImpl.normalizeInput(input, defaultResult);
this.await = norm.await;
this.invoke = norm.invoke;
this.defaultResult = norm.default;
this.onResult = norm.onResult;
this.onError = norm.onError;
}

private await?: MobxPromise_await;
private invoke: MobxPromise_invoke<R>;
private onResult?: (result?: R) => void;
private onError?: (error: Error) => void;
private defaultResult?: R;
private invokeId: number = 0;
private _latestInvokeId: number = 0;

@observable private internalStatus: 'pending' | 'complete' | 'error' =
'pending';
@observable.ref private internalResult?: R = undefined;
@observable.ref private internalError?: Error = undefined;

@computed get status(): 'pending' | 'complete' | 'error' {
// wait until all MobxPromise dependencies are complete
if (this.await)
for (let status of this.await().map(mp => mp.status)) // track all statuses before returning
if (status !== 'complete') return status;

let status = this.internalStatus; // force mobx to track changes to internalStatus
if (this.latestInvokeId != this.invokeId) status = 'pending';
return status;
}

@computed get peekStatus(): 'pending' | 'complete' | 'error' {
// check status without triggering invoke

// check status of all MobxPromise dependencies
if (this.await)
for (let status of this.await().map(mp => mp.peekStatus))
if (status !== 'complete') return status;

// otherwise, return internal status
return this.internalStatus;
}

@computed get isPending() {
return this.status == 'pending';
}

@computed get isComplete() {
return this.status == 'complete';
}

@computed get isError() {
return this.status == 'error';
}

@computed get result(): R | undefined {
// checking status may trigger invoke
if (this.isError || this.internalResult == null)
return this.defaultResult;

return this.internalResult;
}

@computed get error(): Error | undefined {
// checking status may trigger invoke
if (!this.isComplete && this.await)
for (let error of this.await().map(mp => mp.error)) // track all errors before returning
if (error) return error;

return this.internalError;
}

/**
* This lets mobx determine when to call this.invoke(),
* taking advantage of caching based on observable property access tracking.
*/
@computed
private get latestInvokeId() {
window.clearTimeout(this._latestInvokeId);
let promise = this.invoke();
let invokeId: number = window.setTimeout(() =>
this.setPending(invokeId, promise)
);
return (this._latestInvokeId = invokeId);
}

@action
private setPending(invokeId: number, promise: PromiseLike<R>) {
this.invokeId = invokeId;
promise.then(
result => this.setComplete(invokeId, result),
error => this.setError(invokeId, error)
);
this.internalStatus = 'pending';
}

@action
private setComplete(invokeId: number, result: R) {
if (invokeId === this.invokeId) {
this.internalResult = result;
this.internalError = undefined;
this.internalStatus = 'complete';

if (this.onResult) this.onResult(result || this.defaultResult); // may use defaultResult
}
}

@action
private setError(invokeId: number, error: Error) {
if (invokeId === this.invokeId) {
this.internalError = error;
this.internalResult = undefined;
this.internalStatus = 'error';

if (this.onError) this.onError(error);
}
}
}

export type MobxPromiseFactory = {
// This provides more information for TypeScript code flow analysis
<R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseUnionTypeWithDefault<R>;
<R>(
input: MobxPromiseInputUnion<R>,
defaultResult: R
): MobxPromiseUnionTypeWithDefault<R>;
<R>(input: MobxPromiseInputUnion<R>): MobxPromiseUnionType<R>;
};

export const MobxPromise = MobxPromiseImpl as {
// This provides more information for TypeScript code flow analysis
new <R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseUnionTypeWithDefault<R>;
new <R>(
input: MobxPromiseInputUnion<R>,
defaultResult: R
): MobxPromiseUnionTypeWithDefault<R>;
new <R>(input: MobxPromiseInputUnion<R>): MobxPromiseUnionType<R>;
};

export interface MobxPromise<T>
extends Pick<
MobxPromiseImpl<T>,
| 'status'
| 'error'
| 'result'
| 'isPending'
| 'isError'
| 'isComplete'
| 'peekStatus'
> {}
Loading
Loading