Migrate the UserService/Model to not be a singleton
- Rename userService to userModel on appContext
- Merge user-model.ts and user-service.ts
- Rename UserService to UserModel
- Move all observables onto UserModel
- Inject UserModel in the models/services that were directly accessing
the observables
Google-Bug-Id: b/206459178, b/207628953
Change-Id: Icf4f81c877efb58e289678a7331bde7254142a58
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
index f5bbf00..6673cdf 100644
--- a/polygerrit-ui/FE_Style_Guide.md
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -187,11 +187,11 @@
export class MyCustomElement extends ...{
constructor() {
super(); //This is mandatory to call parent constructor
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
}
//...
_getUserName() {
- return this._userService.activeUserName();
+ return this._userModel.activeUserName();
}
}
```
@@ -203,12 +203,12 @@
export class MyCustomElement extends ...{
created() {
// Incorrect: assign all dependencies in the constructor
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
}
//...
_getUserName() {
// Incorrect: use appContext outside of a constructor
- return appContext.userService.activeUserName();
+ return appContext.userModel.activeUserName();
}
}
```
@@ -237,7 +237,7 @@
constructor() {
super();
// Assign services here
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
// Code from the created method - put it before existing actions in constructor
createdAction1();
createdAction2();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index f309171..1076990 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -50,7 +50,6 @@
import {deepClone} from '../../../utils/deep-util';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators';
-import {preferences$} from '../../../services/user/user-model';
import {subscribe} from '../../lit/subscription-controller';
const STATES = {
@@ -122,11 +121,13 @@
@state() private pluginConfigChanged = false;
+ private readonly userModel = getAppContext().userModel;
+
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- subscribe(this, preferences$, prefs => {
+ subscribe(this, this.userModel.preferences$, prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this.selectedScheme = prefs.download_scheme.toLowerCase();
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 0df3a3d..f59fdec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -65,7 +65,6 @@
import {modifierPressed} from '../../../utils/dom-util';
import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
import {fontStyles} from '../../../styles/gr-font-styles';
-import {account$} from '../../../services/user/user-model';
import {
changeComments$,
threads$,
@@ -412,6 +411,8 @@
private showAllChips = new Map<RunStatus | Category, boolean>();
+ private userModel = getAppContext().userModel;
+
private checksService = getAppContext().checksService;
constructor() {
@@ -428,7 +429,7 @@
subscribe(this, topLevelActionsLatest$, x => (this.actions = x));
subscribe(this, changeComments$, x => (this.changeComments = x));
subscribe(this, threads$, x => (this.commentThreads = x));
- subscribe(this, account$, x => (this.selfAccount = x));
+ subscribe(this, this.userModel.account$, x => (this.selfAccount = x));
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 7d8bb86..8b530f6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -202,7 +202,6 @@
hasAttention,
} from '../../../utils/attention-set-util';
import {listen} from '../../../services/shortcuts/shortcuts-service';
-import {preferenceDiffViewMode$} from '../../../services/user/user-model';
import {change$, changeLoading$} from '../../../services/change/change-model';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -562,7 +561,8 @@
readonly restApiService = getAppContext().restApiService;
- private readonly userService = getAppContext().userService;
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
private readonly commentsService = getAppContext().commentsService;
@@ -648,7 +648,7 @@
})
);
this.subscriptions.push(
- preferenceDiffViewMode$.subscribe(diffViewMode => {
+ this.userModel.preferenceDiffViewMode$.subscribe(diffViewMode => {
this.diffViewMode = diffViewMode;
})
);
@@ -790,9 +790,9 @@
_handleToggleDiffMode() {
if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
- this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userService.updatePreferences({
+ this.userModel.updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 6104356..67599ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -27,7 +27,6 @@
MessageTag,
PrimaryTab,
createDefaultPreferences,
- createDefaultDiffPrefs,
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -107,7 +106,6 @@
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
-import {_testOnly_setState as setUserState} from '../../../services/user/user-model';
import {_testOnly_setState as setChangeState} from '../../../services/change/change-model';
import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
@@ -818,10 +816,7 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- setUserState({
- preferences: prefs,
- diffPreferences: createDefaultDiffPrefs(),
- });
+ element.userModel.setPreferences(prefs);
element._handleToggleDiffMode();
assert.isTrue(
updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
@@ -831,10 +826,7 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.UNIFIED,
};
- setUserState({
- preferences: newPrefs,
- diffPreferences: createDefaultDiffPrefs(),
- });
+ element.userModel.setPreferences(newPrefs);
await flush();
element._handleToggleDiffMode();
assert.isTrue(
@@ -1745,6 +1737,7 @@
'#replyDialog'
);
const openSpy = sinon.spy(dialog, 'open');
+ await flush();
await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]);
assert.equal(openSpy.lastCall.args[1], '> quote text\n\n');
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index f400814..f286098 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -83,12 +83,9 @@
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
-import {
- diffPreferences$,
- sizeBarInChangeTable$,
-} from '../../../services/user/user-model';
import {changeComments$} from '../../../services/comments/comments-model';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {select} from '../../../utils/observable-util';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -317,7 +314,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userService = getAppContext().userService;
+ private readonly userModel = getAppContext().userModel;
private readonly browserModel = getAppContext().browserModel;
@@ -377,26 +374,23 @@
override connectedCallback() {
super.connectedCallback();
- this.subscriptions.push(
+ this.subscriptions = [
changeComments$.subscribe(changeComments => {
this.changeComments = changeComments;
- })
- );
- this.subscriptions.push(
+ }),
this.browserModel.diffViewMode$.subscribe(
diffView => (this.diffViewMode = diffView)
- )
- );
- this.subscriptions.push(
- diffPreferences$.subscribe(diffPreferences => {
+ ),
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
this.diffPrefs = diffPreferences;
- })
- );
- this.subscriptions.push(
- sizeBarInChangeTable$.subscribe(sizeBarInChangeTable => {
+ }),
+ select(
+ this.userModel.preferences$,
+ prefs => !!prefs?.size_bar_in_change_table
+ ).subscribe(sizeBarInChangeTable => {
this._showSizeBars = sizeBarInChangeTable;
- })
- );
+ }),
+ ];
getPluginLoader()
.awaitPluginsLoaded()
@@ -1648,7 +1642,7 @@
}
_handleReloadingDiffPreference() {
- this.userService.getDiffPreferences();
+ this.userModel.getDiffPreferences();
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index bd51aab..cedddec 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -57,7 +57,6 @@
changeNum$,
repo$,
} from '../../../services/change/change-model';
-import {loggedIn$} from '../../../services/user/user-model';
/**
* The content of the enum is also used in the UI for the button text.
@@ -263,6 +262,8 @@
@property({type: Object, computed: '_computeLabelExtremes(labels.*)'})
_labelExtremes: {[labelName: string]: VotingRangeInfo} = {};
+ private readonly userModel = getAppContext().userModel;
+
private readonly reporting = getAppContext().reportingService;
private readonly shortcuts = getAppContext().shortcutsService;
@@ -282,7 +283,7 @@
})
);
this.subscriptions.push(
- loggedIn$.subscribe(x => {
+ this.userModel.loggedIn$.subscribe(x => {
this.showReplyButtons = x;
})
);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 29c8eca..1391257 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import {Subscription} from 'rxjs';
+import {map, distinctUntilChanged} from 'rxjs/operators';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../shared/gr-dropdown/gr-dropdown';
import '../../shared/gr-icons/gr-icons';
@@ -37,7 +38,6 @@
import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
import {getAppContext} from '../../../services/app-context';
import {serverConfig$} from '../../../services/config/config-model';
-import {myTopMenuItems$} from '../../../services/user/user-model';
import {assertIsDefined} from '../../../utils/common-util';
type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
@@ -158,7 +158,7 @@
private readonly jsAPI = getAppContext().jsApiService;
- private readonly userService = getAppContext().userService;
+ private readonly userModel = getAppContext().userModel;
private subscriptions: Subscription[] = [];
@@ -168,18 +168,23 @@
}
override connectedCallback() {
- // TODO(brohlfs): This just ensures that the userService is instantiated at
+ // TODO(brohlfs): This just ensures that the userModel is instantiated at
// all. We need the service to manage the model, but we are not making any
// direct calls. Will need to find a better solution to this problem ...
- assertIsDefined(this.userService);
+ assertIsDefined(this.userModel);
super.connectedCallback();
this._loadAccount();
this.subscriptions.push(
- myTopMenuItems$.subscribe(items => {
- this._userLinks = items.map(this._createHeaderLink);
- })
+ this.userModel.preferences$
+ .pipe(
+ map(preferences => preferences?.my ?? []),
+ distinctUntilChanged()
+ )
+ .subscribe(items => {
+ this._userLinks = items.map(this._createHeaderLink);
+ })
);
this.subscriptions.push(
serverConfig$.subscribe(config => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 0d63360..fd30c6a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -50,7 +50,7 @@
// Private but accessed by tests.
readonly browserModel = getAppContext().browserModel;
- private readonly userService = getAppContext().userService;
+ private readonly userModel = getAppContext().userModel;
private subscriptions: Subscription[] = [];
@@ -83,7 +83,7 @@
*/
setMode(newMode: DiffViewMode) {
if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.userService.updatePreferences({diff_view: newMode});
+ this.userModel.updatePreferences({diff_view: newMode});
}
this.mode = newMode;
let announcement;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
index 7f7f265..f469799 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -19,7 +19,6 @@
import './gr-diff-preferences-dialog';
import {GrDiffPreferencesDialog} from './gr-diff-preferences-dialog';
import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {updateDiffPreferences} from '../../../services/user/user-model';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
const basicFixture = fixtureFromElement('gr-diff-preferences-dialog');
@@ -37,7 +36,6 @@
line_wrapping: true,
};
element.diffPrefs = originalDiffPrefs;
- updateDiffPreferences(originalDiffPrefs);
await flush();
element.open();
await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index c4e2488..ca89b1d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -118,10 +118,6 @@
import {Subscription, combineLatest} from 'rxjs';
import {listen} from '../../../services/shortcuts/shortcuts-service';
import {
- preferences$,
- diffPreferences$,
-} from '../../../services/user/user-model';
-import {
diffPath$,
currentPatchNum$,
change$,
@@ -359,11 +355,12 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userService = getAppContext().userService;
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
private readonly changeService = getAppContext().changeService;
- // Private but used in tests
+ // Private but used in tests.
readonly browserModel = getAppContext().browserModel;
// We just want to make sure that CommentsService is instantiated.
@@ -401,12 +398,12 @@
);
this.subscriptions.push(
- preferences$.subscribe(preferences => {
+ this.userModel.preferences$.subscribe(preferences => {
this._userPrefs = preferences;
})
);
this.subscriptions.push(
- diffPreferences$.subscribe(diffPreferences => {
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
this._prefs = diffPreferences;
})
);
@@ -423,7 +420,12 @@
// properties since the method will be called anytime a property updates
// but we only want to call this on the initial load.
this.subscriptions.push(
- combineLatest(currentPatchNum$, routerView$, diffPath$, diffPreferences$)
+ combineLatest(
+ currentPatchNum$,
+ routerView$,
+ diffPath$,
+ this.userModel.diffPreferences$
+ )
.pipe(
filter(
([currentPatchNum, routerView, path, diffPrefs]) =>
@@ -784,9 +786,9 @@
_handleToggleDiffMode() {
if (!this._userPrefs) return;
if (this._userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
- this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userService.updatePreferences({
+ this.userModel.updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
@@ -1764,7 +1766,7 @@
}
_handleReloadingDiffPreference() {
- this.userService.getDiffPreferences();
+ this.userModel.getDiffPreferences();
}
_computeCanEdit(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 2367342..9076420 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -31,7 +31,6 @@
import {EditPatchSetNum} from '../../../types/common.js';
import {CursorMoveResult} from '../../../api/core.js';
import {Side} from '../../../api/diff.js';
-import {_testOnly_setState as setUserModelState, _testOnly_getState as getUserModelState} from '../../../services/user/user-model.js';
import {_testOnly_setState as setChangeModelState} from '../../../services/change/change-model.js';
import {_testOnly_setState as setCommentState} from '../../../services/comments/comments-model.js';
@@ -1199,7 +1198,7 @@
...createDefaultDiffPrefs(),
manual_review: true,
};
- setUserModelState({...getUserModelState(), diffPreferences});
+ element.userModel.setDiffPreferences(diffPreferences);
setChangeModelState({change: createChange(), diffPath: '/COMMIT_MSG'});
setRouterModelState({
@@ -1216,8 +1215,7 @@
assert.isTrue(getReviewedStub.called);
// if prefs are updated then the reviewed status should not be set again
- setUserModelState({...getUserModelState(),
- diffPreferences: createDefaultDiffPrefs()});
+ element.userModel.setDiffPreferences(createDefaultDiffPrefs());
await flush();
assert.isFalse(saveReviewedStub.called);
@@ -1237,7 +1235,7 @@
...createDefaultDiffPrefs(),
manual_review: false,
};
- setUserModelState({...getUserModelState(), diffPreferences});
+ element.userModel.setDiffPreferences(diffPreferences);
setChangeModelState({change: createChange(),
diffPath: '/COMMIT_MSG'});
@@ -1262,8 +1260,7 @@
.callsFake(() => Promise.resolve());
sinon.stub(element.$.diffHost, 'reload');
- setUserModelState({...getUserModelState(),
- diffPreferences: createDefaultDiffPrefs()});
+ element.userModel.setDiffPreferences(createDefaultDiffPrefs());
setChangeModelState({change: createChange(), diffPath: '/COMMIT_MSG'});
setRouterModelState({
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 4657020..23d9693 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -25,7 +25,6 @@
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
import {GrSelect} from '../gr-select/gr-select';
import {getAppContext} from '../../../services/app-context';
-import {diffPreferences$} from '../../../services/user/user-model';
export interface GrDiffPreferences {
$: {
@@ -56,14 +55,14 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- private readonly userService = getAppContext().userService;
+ private readonly userModel = getAppContext().userModel;
private subscriptions: Subscription[] = [];
override connectedCallback() {
super.connectedCallback();
this.subscriptions.push(
- diffPreferences$.subscribe(diffPreferences => {
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
this.diffPrefs = diffPreferences;
})
);
@@ -142,7 +141,7 @@
async save() {
if (!this.diffPrefs) return;
- await this.userService.updateDiffPreference(this.diffPrefs);
+ await this.userModel.updateDiffPreference(this.diffPrefs);
this.hasUnsavedChanges = false;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 68d8d7d..6eb19da 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -27,7 +27,6 @@
import {getAppContext} from '../../../services/app-context';
import {queryAndAssert} from '../../../utils/common-util';
import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
-import {preferences$} from '../../../services/user/user-model';
declare global {
interface HTMLElementEventMap {
@@ -73,7 +72,8 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userService = getAppContext().userService;
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
private subscriptions: Subscription[] = [];
@@ -83,7 +83,7 @@
this._loggedIn = loggedIn;
});
this.subscriptions.push(
- preferences$.subscribe(prefs => {
+ this.userModel.preferences$.subscribe(prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this.selectedScheme = prefs.download_scheme.toLowerCase();
@@ -113,7 +113,7 @@
if (scheme && scheme !== this.selectedScheme) {
this.set('selectedScheme', scheme);
if (this._loggedIn) {
- this.userService.updatePreferences({
+ this.userModel.updatePreferences({
download_scheme: this.selectedScheme,
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 6cbef79..bd0ca70 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -19,7 +19,6 @@
import './gr-download-commands';
import {GrDownloadCommands} from './gr-download-commands';
import {isHidden, queryAndAssert, stubRestApi} from '../../../test/test-utils';
-import {updatePreferences} from '../../../services/user/user-model';
import {createPreferences} from '../../../test/test-data-generators';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
@@ -116,7 +115,7 @@
test('loads scheme from preferences', async () => {
const element = basicFixture.instantiate();
await flush();
- updatePreferences({
+ element.userModel.setPreferences({
...createPreferences(),
download_scheme: 'repo',
});
@@ -126,7 +125,7 @@
test('normalize scheme from preferences', async () => {
const element = basicFixture.instantiate();
await flush();
- updatePreferences({
+ element.userModel.setPreferences({
...createPreferences(),
download_scheme: 'REPO',
});
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 47d5f03..001b71d 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -26,7 +26,7 @@
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
import {GrStorageService} from './storage/gr-storage_impl';
import {ConfigService} from './config/config-service';
-import {UserService} from './user/user-service';
+import {UserModel} from './user/user-model';
import {CommentsService} from './comments/comments-service';
import {ShortcutsService} from './shortcuts/shortcuts-service';
import {BrowserModel} from './browser/browser-model';
@@ -74,15 +74,19 @@
assertIsDefined(ctx.restApiService, 'restApiService');
return new ConfigService(ctx.restApiService!);
},
- userService: (ctx: Partial<AppContext>) => {
+ userModel: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserService(ctx.restApiService!);
+ return new UserModel(ctx.restApiService!);
},
shortcutsService: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
assertIsDefined(ctx.reportingService, 'reportingService');
- return new ShortcutsService(ctx.reportingService!);
+ return new ShortcutsService(ctx.userModel, ctx.reportingService!);
},
- browserModel: (_ctx: Partial<AppContext>) => new BrowserModel(),
+ browserModel: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
+ return new BrowserModel(ctx.userModel!);
+ },
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 87a18ef..d5e595d 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -25,7 +25,7 @@
import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
import {StorageService} from './storage/gr-storage';
import {ConfigService} from './config/config-service';
-import {UserService} from './user/user-service';
+import {UserModel} from './user/user-model';
import {CommentsService} from './comments/comments-service';
import {ShortcutsService} from './shortcuts/shortcuts-service';
import {BrowserModel} from './browser/browser-model';
@@ -42,7 +42,7 @@
jsApiService: JsApiService;
storageService: StorageService;
configService: ConfigService;
- userService: UserService;
+ userModel: UserModel;
browserModel: BrowserModel;
shortcutsService: ShortcutsService;
}
diff --git a/polygerrit-ui/app/services/browser/browser-model.ts b/polygerrit-ui/app/services/browser/browser-model.ts
index b15091a..a675cdd 100644
--- a/polygerrit-ui/app/services/browser/browser-model.ts
+++ b/polygerrit-ui/app/services/browser/browser-model.ts
@@ -17,8 +17,8 @@
import {BehaviorSubject, Observable, combineLatest} from 'rxjs';
import {distinctUntilChanged, map} from 'rxjs/operators';
import {Finalizable} from '../registry';
-import {preferenceDiffViewMode$} from '../user/user-model';
import {DiffViewMode} from '../../api/diff';
+import {UserModel} from '../user/user-model';
// This value is somewhat arbitrary and not based on research or calculations.
const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
@@ -42,7 +42,7 @@
return this.privateState$;
}
- constructor() {
+ constructor(readonly userModel: UserModel) {
const screenWidth$ = this.privateState$.pipe(
map(
state =>
@@ -55,7 +55,7 @@
// the user model.
this.diffViewMode$ = combineLatest([
screenWidth$,
- preferenceDiffViewMode$,
+ userModel.preferenceDiffViewMode$,
]).pipe(
map(([isScreenTooSmall, preferenceDiffViewMode]) => {
if (isScreenTooSmall) return DiffViewMode.UNIFIED;
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 1f9e083..8f14077 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -15,13 +15,13 @@
* limitations under the License.
*/
import {Subscription} from 'rxjs';
+import {map, distinctUntilChanged} from 'rxjs/operators';
import {
config,
Shortcut,
ShortcutHelpItem,
ShortcutSection,
} from './shortcuts-config';
-import {disableShortcuts$} from '../user/user-model';
import {
ComboKey,
eventMatchesShortcut,
@@ -33,6 +33,7 @@
} from '../../utils/dom-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
import {Finalizable} from '../registry';
+import {UserModel} from '../user/user-model';
export type SectionView = Array<{binding: string[][]; text: string}>;
@@ -98,7 +99,10 @@
private readonly subscriptions: Subscription[] = [];
- constructor(readonly reporting?: ReportingService) {
+ constructor(
+ readonly userModel: UserModel,
+ readonly reporting?: ReportingService
+ ) {
for (const section of config.keys()) {
const items = config.get(section) ?? [];
for (const item of items) {
@@ -106,7 +110,12 @@
}
}
this.subscriptions.push(
- disableShortcuts$.subscribe(x => (this.shortcutsDisabled = x))
+ this.userModel.preferences$
+ .pipe(
+ map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
+ distinctUntilChanged()
+ )
+ .subscribe(x => (this.shortcutsDisabled = x))
);
this.keydownListener = (e: KeyboardEvent) => {
if (!isComboKey(e.key)) return;
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index a024159..274cb87 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -24,6 +24,7 @@
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {SinonFakeTimers} from 'sinon';
import {Key, Modifier} from '../../utils/dom-util';
+import {getAppContext} from '../app-context';
async function keyEventOn(
el: HTMLElement,
@@ -45,7 +46,10 @@
let service: ShortcutsService;
setup(() => {
- service = new ShortcutsService();
+ service = new ShortcutsService(
+ getAppContext().userModel,
+ getAppContext().reportingService
+ );
});
suite('shouldSuppress', () => {
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index f772ebe..0b6ef6b 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -14,16 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import {from, of, BehaviorSubject, Observable, Subscription} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {
+ DiffPreferencesInfo as DiffPreferencesInfoAPI,
+ DiffViewMode,
+} from '../../api/diff';
import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
-import {BehaviorSubject, Observable} from 'rxjs';
-import {map, distinctUntilChanged} from 'rxjs/operators';
import {
createDefaultPreferences,
createDefaultDiffPrefs,
} from '../../constants/constants';
-import {DiffPreferencesInfo, DiffViewMode} from '../../api/diff';
+import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {DiffPreferencesInfo} from '../../types/diff';
+import {Finalizable} from '../registry';
+import {select} from '../../utils/observable-util';
-interface UserState {
+export interface UserState {
/**
* Keeps being defined even when credentials have expired.
*/
@@ -32,83 +40,122 @@
diffPreferences: DiffPreferencesInfo;
}
-const initialState: UserState = {
- preferences: createDefaultPreferences(),
- diffPreferences: createDefaultDiffPrefs(),
-};
+export class UserModel implements Finalizable {
+ private readonly privateState$: BehaviorSubject<UserState> =
+ new BehaviorSubject({
+ preferences: createDefaultPreferences(),
+ diffPreferences: createDefaultDiffPrefs(),
+ });
-const privateState$ = new BehaviorSubject(initialState);
+ readonly account$: Observable<AccountDetailInfo | undefined> = select(
+ this.privateState$,
+ userState => userState.account
+ );
-export function _testOnly_resetState() {
- // We cannot assign a new subject to privateState$, because all the selectors
- // have already subscribed to the original subject. So we have to emit the
- // initial state on the existing subject.
- privateState$.next({...initialState});
+ /** Note that this may still be true, even if credentials have expired. */
+ readonly loggedIn$: Observable<boolean> = select(
+ this.account$,
+ account => !!account
+ );
+
+ readonly preferences$: Observable<PreferencesInfo> = select(
+ this.privateState$,
+ userState => userState.preferences
+ );
+
+ readonly diffPreferences$: Observable<DiffPreferencesInfo> = select(
+ this.privateState$,
+ userState => userState.diffPreferences
+ );
+
+ readonly preferenceDiffViewMode$: Observable<DiffViewMode> = select(
+ this.preferences$,
+ preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE
+ );
+
+ private readonly subscriptions: Subscription[] = [];
+
+ get userState$(): Observable<UserState> {
+ return this.privateState$;
+ }
+
+ constructor(readonly restApiService: RestApiService) {
+ this.subscriptions = [
+ from(this.restApiService.getAccount()).subscribe(
+ (account?: AccountDetailInfo) => {
+ this.setAccount(account);
+ }
+ ),
+ this.account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultPreferences());
+ return from(this.restApiService.getPreferences());
+ })
+ )
+ .subscribe((preferences?: PreferencesInfo) => {
+ this.setPreferences(preferences ?? createDefaultPreferences());
+ }),
+ this.account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultDiffPrefs());
+ return from(this.restApiService.getDiffPreferences());
+ })
+ )
+ .subscribe((diffPrefs?: DiffPreferencesInfoAPI) => {
+ this.setDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
+ }),
+ ];
+ }
+
+ finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions.splice(0, this.subscriptions.length);
+ }
+
+ updatePreferences(prefs: Partial<PreferencesInfo>) {
+ this.restApiService
+ .savePreferences(prefs)
+ .then((newPrefs: PreferencesInfo | undefined) => {
+ if (!newPrefs) return;
+ this.setPreferences(newPrefs);
+ });
+ }
+
+ updateDiffPreference(diffPrefs: DiffPreferencesInfo) {
+ return this.restApiService
+ .saveDiffPreferences(diffPrefs)
+ .then((response: Response) => {
+ this.restApiService.getResponseObject(response).then(obj => {
+ const newPrefs = obj as unknown as DiffPreferencesInfo;
+ if (!newPrefs) return;
+ this.setDiffPreferences(newPrefs);
+ });
+ });
+ }
+
+ getDiffPreferences() {
+ return this.restApiService.getDiffPreferences().then(prefs => {
+ if (!prefs) return;
+ this.setDiffPreferences(prefs);
+ });
+ }
+
+ setPreferences(preferences: PreferencesInfo) {
+ const current = this.privateState$.getValue();
+ this.privateState$.next({...current, preferences});
+ }
+
+ setDiffPreferences(diffPreferences: DiffPreferencesInfo) {
+ const current = this.privateState$.getValue();
+ this.privateState$.next({...current, diffPreferences});
+ }
+
+ private setAccount(account?: AccountDetailInfo) {
+ const current = this.privateState$.getValue();
+ this.privateState$.next({...current, account});
+ }
}
-
-export function _testOnly_setState(state: UserState) {
- privateState$.next(state);
-}
-
-export function _testOnly_getState() {
- return privateState$.getValue();
-}
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const userState$: Observable<UserState> = privateState$;
-
-export function updateAccount(account?: AccountDetailInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, account});
-}
-
-export function updatePreferences(preferences: PreferencesInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, preferences});
-}
-
-export function updateDiffPreferences(diffPreferences: DiffPreferencesInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, diffPreferences});
-}
-
-export const account$ = userState$.pipe(
- map(userState => userState.account),
- distinctUntilChanged()
-);
-
-/** Note that this may still be true, even if credentials have expired. */
-export const loggedIn$ = account$.pipe(
- map(account => !!account),
- distinctUntilChanged()
-);
-
-export const preferences$ = userState$.pipe(
- map(userState => userState.preferences),
- distinctUntilChanged()
-);
-
-export const diffPreferences$ = userState$.pipe(
- map(userState => userState.diffPreferences),
- distinctUntilChanged()
-);
-
-export const preferenceDiffViewMode$ = preferences$.pipe(
- map(preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE),
- distinctUntilChanged()
-);
-
-export const myTopMenuItems$ = preferences$.pipe(
- map(preferences => preferences?.my ?? []),
- distinctUntilChanged()
-);
-
-export const sizeBarInChangeTable$ = preferences$.pipe(
- map(prefs => !!prefs?.size_bar_in_change_table),
- distinctUntilChanged()
-);
-
-export const disableShortcuts$ = preferences$.pipe(
- map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
- distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/user/user-service.ts b/polygerrit-ui/app/services/user/user-service.ts
deleted file mode 100644
index d2bca85..0000000
--- a/polygerrit-ui/app/services/user/user-service.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {from, of, Subscription} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
-import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
-import {
- account$,
- updateAccount,
- updatePreferences,
- updateDiffPreferences,
-} from './user-model';
-import {
- createDefaultPreferences,
- createDefaultDiffPrefs,
-} from '../../constants/constants';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {DiffPreferencesInfo} from '../../types/diff';
-import {Finalizable} from '../registry';
-
-export class UserService implements Finalizable {
- private readonly subscriptions: Subscription[] = [];
-
- constructor(readonly restApiService: RestApiService) {
- from(this.restApiService.getAccount()).subscribe(
- (account?: AccountDetailInfo) => {
- updateAccount(account);
- }
- );
- this.subscriptions.push(
- account$
- .pipe(
- switchMap(account => {
- if (!account) return of(createDefaultPreferences());
- return from(this.restApiService.getPreferences());
- })
- )
- .subscribe((preferences?: PreferencesInfo) => {
- updatePreferences(preferences ?? createDefaultPreferences());
- })
- );
- this.subscriptions.push(
- account$
- .pipe(
- switchMap(account => {
- if (!account) return of(createDefaultDiffPrefs());
- return from(this.restApiService.getDiffPreferences());
- })
- )
- .subscribe((diffPrefs?: DiffPreferencesInfo) => {
- updateDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
- })
- );
- }
-
- finalize() {
- for (const s of this.subscriptions) {
- s.unsubscribe();
- }
- this.subscriptions.splice(0, this.subscriptions.length);
- }
-
- updatePreferences(prefs: Partial<PreferencesInfo>) {
- this.restApiService
- .savePreferences(prefs)
- .then((newPrefs: PreferencesInfo | undefined) => {
- if (!newPrefs) return;
- updatePreferences(newPrefs);
- });
- }
-
- updateDiffPreference(diffPrefs: DiffPreferencesInfo) {
- return this.restApiService
- .saveDiffPreferences(diffPrefs)
- .then((response: Response) => {
- this.restApiService.getResponseObject(response).then(obj => {
- const newPrefs = obj as unknown as DiffPreferencesInfo;
- if (!newPrefs) return;
- updateDiffPreferences(newPrefs);
- });
- });
- }
-
- getDiffPreferences() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- if (!prefs) return;
- updateDiffPreferences(prefs);
- });
- }
-}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index f8fd534..f041354 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -46,14 +46,12 @@
} from '../scripts/polymer-resin-install';
import {_testOnly_allTasks} from '../utils/async-util';
import {cleanUpStorage} from '../services/storage/gr-storage_mock';
-import {updatePreferences} from '../services/user/user-model';
-import {createDefaultPreferences} from '../constants/constants';
+
import {getAppContext} from '../services/app-context';
import {_testOnly_resetState as resetChangeState} from '../services/change/change-model';
import {_testOnly_resetState as resetChecksState} from '../services/checks/checks-model';
import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model';
import {_testOnly_resetState as resetRouterState} from '../services/router/router-model';
-import {_testOnly_resetState as resetUserState} from '../services/user/user-model';
declare global {
interface Window {
@@ -122,7 +120,6 @@
resetChecksState();
resetCommentsState();
resetRouterState();
- resetUserState();
const shortcuts = getAppContext().shortcutsService;
assert.isTrue(shortcuts._testOnly_isEmpty());
@@ -220,7 +217,6 @@
cancelAllTasks();
cleanUpStorage();
// Reset state
- updatePreferences(createDefaultPreferences());
_testOnlyFinalizeAppContext();
const testTeardownTimestampMs = new Date().getTime();
const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 03c5967..a710b00 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -29,7 +29,7 @@
import {ChecksService} from '../services/checks/checks-service';
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
import {ConfigService} from '../services/config/config-service';
-import {UserService} from '../services/user/user-service';
+import {UserModel} from '../services/user/user-model';
import {CommentsService} from '../services/comments/comments-service';
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
import {BrowserModel} from '../services/browser/browser-model';
@@ -68,15 +68,19 @@
assertIsDefined(ctx.restApiService, 'restApiService');
return new ConfigService(ctx.restApiService!);
},
- userService: (ctx: Partial<AppContext>) => {
+ userModel: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserService(ctx.restApiService!);
+ return new UserModel(ctx.restApiService!);
},
shortcutsService: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
assertIsDefined(ctx.reportingService, 'reportingService');
- return new ShortcutsService(ctx.reportingService!);
+ return new ShortcutsService(ctx.userModel!, ctx.reportingService!);
},
- browserModel: (_ctx: Partial<AppContext>) => new BrowserModel(),
+ browserModel: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
+ return new BrowserModel(ctx.userModel!);
+ },
};
appContext = create<AppContext>(appRegistry);
injectAppContext(appContext);
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 2b57e98..f2da972 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -24,7 +24,7 @@
import {AuthService} from '../services/gr-auth/gr-auth';
import {ReportingService} from '../services/gr-reporting/gr-reporting';
import {CommentsService} from '../services/comments/comments-service';
-import {UserService} from '../services/user/user-service';
+import {UserModel} from '../services/user/user-model';
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
@@ -116,8 +116,8 @@
return sinon.stub(getAppContext().commentsService, method);
}
-export function stubUsers<K extends keyof UserService>(method: K) {
- return sinon.stub(getAppContext().userService, method);
+export function stubUsers<K extends keyof UserModel>(method: K) {
+ return sinon.stub(getAppContext().userModel, method);
}
export function stubShortcuts<K extends keyof ShortcutsService>(method: K) {
diff --git a/polygerrit-ui/app/utils/observable-util.ts b/polygerrit-ui/app/utils/observable-util.ts
new file mode 100644
index 0000000..e39aa48
--- /dev/null
+++ b/polygerrit-ui/app/utils/observable-util.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Observable} from 'rxjs';
+import {distinctUntilChanged, map, shareReplay} from 'rxjs/operators';
+import {deepEqual} from './deep-util';
+
+export function select<A, B>(obs$: Observable<A>, mapper: (_: A) => B) {
+ return obs$.pipe(
+ map(mapper),
+ distinctUntilChanged(deepEqual),
+ shareReplay(1)
+ );
+}