From c529631483958931299209687fed318fdd48c11a Mon Sep 17 00:00:00 2001 From: Felix Kling Date: Mon, 18 Mar 2024 14:02:57 +0100 Subject: [PATCH] web: Update rxjs to v7 (#61122) A side goal for the web rewrite is to leave the existing code base in a better state than before. I recently [added a hacky workaround](https://github.com/sourcegraph/sourcegraph/blob/da5ddc99b65398c3cb14f075db217f1fb374095a/client/web-sveltekit/vite.config.ts#L82-L101) to make the Svelte version work properly with different rxjs versions. But the whole point of the rewrite is to not have to do these things anymore. So this is my attempt of upgrading rsjx in the main repo to v7. I worked through the list of breaking changes in the [rxjs documentation](https://rxjs.dev/deprecations/breaking-changes) and fixed TypeScript issues to the best of my abilities. Most notable changes: - The custom `combineLatestOrDefault` operator was rewritten to avoid using rxjs internals, and the `.lift` method (side note: the corresponding tests do not cover all expected behavior, but issues had been caught through other tests) - Where necessary `.toPromise()` was replaced with `lastValueFrom` or `firstValueFrom`. My assumption was that since we don't get runtime errors for the existing code, it's save to assume that the corresponding observables emit at least one value, i.e. `.toPromise()` did not return `undefined`. Only in some places I added a default value where it was easy to deduce what it should be. - The generic type in `of()` was removed - The generic type in `concat()` was removed - `Subject.next` seemed to have allowed `undefined` to be passed even if the subject's types didn't allow that. If the subject's type couldn't be extended to include `undefined` I changed the code to not pass `undefined`. - The generic type signature of `defaultIfEmpty` changed. - Where possible I replaced `Subscribable` with `ObservableInput`, but we also have a copy of the old `Subscribable` interface in the `sourcegraph` package, and that makes things more complicated. - I simplified unnecessary Promise/Observable interop where necessary. A lot of the complex rxjs logic and usage of changed interfaces, such as `Subscribable`, is in extensions related code, which is not used in the web app anymore, but is still at least imported in the browser extensions code. Most of it is probably not used anymore, which makes the migration somewhat simpler. --- .../scripts/backgroundPage.main.ts | 4 +- client/browser/src/end-to-end/github.test.ts | 13 +- client/browser/src/shared/cli/search.ts | 9 +- .../src/shared/code-hosts/gitlab/codeHost.ts | 30 +- .../shared/code-hosts/phabricator/backend.tsx | 8 +- .../code-hosts/phabricator/fileInfo.test.ts | 22 +- .../src/shared/code-hosts/shared/codeHost.tsx | 11 +- .../code-hosts/shared/codeViews.test.ts | 14 +- .../shared/code-hosts/shared/views.test.ts | 52 +-- .../browser/src/shared/extensionHostWorker.ts | 5 +- client/browser/src/shared/platform/context.ts | 4 +- .../src/esbuild/packageResolutionPlugin.ts | 11 - .../build-config/src/esbuild/workerPlugin.ts | 3 +- client/codeintellify/src/helpers.ts | 6 +- client/codeintellify/src/hoverifier.test.ts | 11 +- client/codeintellify/src/hoverifier.ts | 27 +- client/codeintellify/src/positions.ts | 4 +- .../codeintellify/src/testutils/fixtures.ts | 8 +- client/common/BUILD.bazel | 2 + .../src/util/rxjs/combineLatestOrDefault.ts | 138 ++++---- .../common/src/util/rxjs/fromSubscribable.ts | 14 + client/common/src/util/rxjs/index.ts | 1 + .../src/sourcegraph-api-access/api-gateway.ts | 28 +- client/shared/src/actions/ActionsNavItems.tsx | 4 +- client/shared/src/api/client/api/common.ts | 44 +-- client/shared/src/api/client/connection.ts | 5 +- .../src/api/client/mainthread-api.test.ts | 2 +- .../shared/src/api/client/mainthread-api.ts | 8 +- .../src/api/client/services/settings.ts | 5 +- client/shared/src/api/contract.ts | 3 +- client/shared/src/api/extension/api/common.ts | 12 +- .../src/api/extension/extensionHostApi.ts | 8 +- .../extensionHost.documentHighlights.test.ts | 1 + .../api/integration-test/codeEditor.test.ts | 13 +- .../api/integration-test/documents.test.ts | 4 +- .../src/api/integration-test/roots.test.ts | 4 +- .../api/integration-test/selections.test.ts | 9 +- .../src/api/integration-test/windows.test.ts | 14 +- client/shared/src/api/util.ts | 10 +- client/shared/src/codeintel/api.ts | 70 ++-- .../src/codeintel/legacy-extensions/api.ts | 9 +- client/shared/src/hover/actions.test.ts | 10 +- client/shared/src/hover/actions.ts | 23 +- client/shared/src/platform/context.ts | 4 +- .../shared/src/testing/integration/context.ts | 8 +- client/shared/src/testing/testHelpers.ts | 7 +- client/shared/src/util/useInputValidation.ts | 2 +- client/web-sveltekit/src/lib/graphql/urql.ts | 2 +- client/web-sveltekit/vite.config.ts | 27 -- client/web/dev/BUILD.bazel | 4 - client/web/dev/esbuild/config.ts | 2 - client/web/src/SearchQueryStateObserver.tsx | 16 +- .../components/externalServices/backend.ts | 34 +- .../src/enterprise/batches/close/backend.ts | 22 +- .../src/enterprise/batches/detail/backend.ts | 238 +++++++------ .../src/enterprise/batches/preview/backend.ts | 64 ++-- .../indexes/components/EnqueueForm.tsx | 4 +- .../pages/CodeIntelPreciseIndexesPage.tsx | 2 +- .../use-live-preview-lang-stats-insight.ts | 2 +- .../InsightsDashboardCreationPage.tsx | 3 +- .../edit-dashboard/EditDashobardPage.tsx | 17 +- .../src/enterprise/searchContexts/backend.ts | 18 +- client/web/src/org/backend.ts | 28 +- .../src/org/settings/members/InviteForm.tsx | 35 +- client/web/src/regression/util/api.ts | 315 +++++++++--------- client/web/src/regression/util/helpers.ts | 14 +- client/web/src/repo/RepoContainer.tsx | 4 +- client/web/src/repo/blame/useBlameHunks.ts | 36 +- .../repo/blob/actions/ToggleHistoryPanel.tsx | 2 +- .../src/repo/settings/RepoSettingsArea.tsx | 7 +- .../site-admin/SiteAdminConfigurationPage.tsx | 6 +- .../components/useUserListActions.tsx | 5 +- client/web/src/tour/components/Tour/utils.tsx | 28 +- .../auth/RemoveExternalAccountModal.tsx | 22 +- client/web/src/user/settings/backend.tsx | 11 +- .../user/settings/emails/AddUserEmailForm.tsx | 21 +- .../emails/SetUserPrimaryEmailForm.tsx | 21 +- .../src/user/settings/emails/UserEmail.tsx | 62 ++-- .../emails/UserSettingsEmailsPage.tsx | 41 +-- package.json | 2 +- pnpm-lock.yaml | 5 +- 81 files changed, 910 insertions(+), 914 deletions(-) create mode 100644 client/common/src/util/rxjs/fromSubscribable.ts diff --git a/client/browser/src/browser-extension/scripts/backgroundPage.main.ts b/client/browser/src/browser-extension/scripts/backgroundPage.main.ts index c37a379b52e..7f7fc83296d 100644 --- a/client/browser/src/browser-extension/scripts/backgroundPage.main.ts +++ b/client/browser/src/browser-extension/scripts/backgroundPage.main.ts @@ -5,7 +5,7 @@ import '../../config/background.entry' import '../../shared/polyfills' import type { Endpoint } from 'comlink' -import { combineLatest, merge, type Observable, of, Subject, Subscription, timer } from 'rxjs' +import { combineLatest, merge, type Observable, of, Subject, Subscription, timer, lastValueFrom } from 'rxjs' import { bufferCount, filter, @@ -238,7 +238,7 @@ async function main(): Promise { variables: V sourcegraphURL?: string }): Promise> { - return requestGraphQL({ request, variables, sourcegraphURL }).toPromise() + return lastValueFrom(requestGraphQL({ request, variables, sourcegraphURL })) }, async notifyRepoSyncError({ sourcegraphURL, hasRepoSyncError }, sender: browser.runtime.MessageSender) { diff --git a/client/browser/src/end-to-end/github.test.ts b/client/browser/src/end-to-end/github.test.ts index b09adac2e3f..5814cc7a0aa 100644 --- a/client/browser/src/end-to-end/github.test.ts +++ b/client/browser/src/end-to-end/github.test.ts @@ -3,8 +3,8 @@ import assert from 'assert' import { startCase } from 'lodash' import { describe, it } from 'mocha' import type { Target, Page } from 'puppeteer' -import { fromEvent } from 'rxjs' -import { first, filter, timeout, mergeMap } from 'rxjs/operators' +import { firstValueFrom, fromEvent } from 'rxjs' +import { filter, timeout, mergeMap } from 'rxjs/operators' import { isDefined } from '@sourcegraph/common' import { getConfig } from '@sourcegraph/shared/src/testing/config' @@ -141,14 +141,13 @@ describe('Sourcegraph browser extension on github.com', function () { let page: Page = driver.page if (new URL(goToDefinitionURL).hostname !== 'github.com') { ;[page] = await Promise.all([ - fromEvent(driver.browser, 'targetcreated') - .pipe( + firstValueFrom( + fromEvent(driver.browser, 'targetcreated').pipe( timeout(5000), mergeMap(target => target.page()), - filter(isDefined), - first() + filter(isDefined) ) - .toPromise(), + ), driver.page.click('.test-tooltip-go-to-definition'), ]) } else { diff --git a/client/browser/src/shared/cli/search.ts b/client/browser/src/shared/cli/search.ts index 065f5497d5d..ea1a5bad330 100644 --- a/client/browser/src/shared/cli/search.ts +++ b/client/browser/src/shared/cli/search.ts @@ -1,5 +1,4 @@ -import { from } from 'rxjs' -import { take } from 'rxjs/operators' +import { firstValueFrom } from 'rxjs' import { type ErrorLike, isErrorLike, isDefined, isNot } from '@sourcegraph/common' import type { Settings } from '@sourcegraph/shared/src/settings/settings' @@ -22,7 +21,7 @@ export class SearchCommand { private prev: { query: string; suggestions: browser.omnibox.SuggestResult[] } = { query: '', suggestions: [] } public getSuggestions = async (query: string): Promise => { - const sourcegraphURL = await observeSourcegraphURL(IS_EXTENSION).pipe(take(1)).toPromise() + const sourcegraphURL = await firstValueFrom(observeSourcegraphURL(IS_EXTENSION)) return new Promise(resolve => { if (this.prev.query === query) { resolve(this.prev.suggestions) @@ -54,7 +53,7 @@ export class SearchCommand { disposition?: 'newForegroundTab' | 'newBackgroundTab' | 'currentTab', currentTabId?: number ): Promise => { - const sourcegraphURL = await observeSourcegraphURL(IS_EXTENSION).pipe(take(1)).toPromise() + const sourcegraphURL = await firstValueFrom(observeSourcegraphURL(IS_EXTENSION)) const [patternType, caseSensitive] = await this.getDefaultSearchSettings(sourcegraphURL) @@ -121,7 +120,7 @@ export class SearchCommand { ) await platformContext.refreshSettings() - const settings = (await from(platformContext.settings).pipe(take(1)).toPromise()).final + const settings = (await firstValueFrom(platformContext.settings)).final if (isDefined(settings) && isNot(isErrorLike)(settings)) { this.defaultPatternType = diff --git a/client/browser/src/shared/code-hosts/gitlab/codeHost.ts b/client/browser/src/shared/code-hosts/gitlab/codeHost.ts index 4f0d70f314a..d044435be1d 100644 --- a/client/browser/src/shared/code-hosts/gitlab/codeHost.ts +++ b/client/browser/src/shared/code-hosts/gitlab/codeHost.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/browser' import classNames from 'classnames' -import { fromEvent } from 'rxjs' +import { fromEvent, lastValueFrom } from 'rxjs' import { filter, map, mapTo, tap } from 'rxjs/operators' import type { Omit } from 'utility-types' @@ -286,25 +286,25 @@ export const gitlabCodeHost = subtypeOf()({ ), prepareCodeHost: async requestGraphQL => - requestGraphQL({ - request: gql` - query ResolveRepoName($cloneURL: String!) { - repository(cloneURL: $cloneURL) { - name + lastValueFrom( + requestGraphQL({ + request: gql` + query ResolveRepoName($cloneURL: String!) { + repository(cloneURL: $cloneURL) { + name + } } - } - `, - variables: { - cloneURL: getGitlabRepoURL(), - }, - mightContainPrivateInfo: true, - }) - .pipe( + `, + variables: { + cloneURL: getGitlabRepoURL(), + }, + mightContainPrivateInfo: true, + }).pipe( map(dataOrThrowErrors), tap(({ repository }) => { repoNameOnSourcegraph.next(repository?.name ?? '') }), mapTo(true) ) - .toPromise(), + ), }) diff --git a/client/browser/src/shared/code-hosts/phabricator/backend.tsx b/client/browser/src/shared/code-hosts/phabricator/backend.tsx index 70a43f30440..140b9df957d 100644 --- a/client/browser/src/shared/code-hosts/phabricator/backend.tsx +++ b/client/browser/src/shared/code-hosts/phabricator/backend.tsx @@ -1,4 +1,4 @@ -import { from, type Observable, of, throwError } from 'rxjs' +import { from, type Observable, of, throwError, lastValueFrom } from 'rxjs' import { fromFetch } from 'rxjs/fetch' import { map, mapTo, switchMap, catchError } from 'rxjs/operators' @@ -275,9 +275,9 @@ export function getRepoDetailsFromCallsign( * case it fails we query the conduit API. */ export function getSourcegraphURLFromConduit(): Promise { - return queryConduitHelper<{ url: string }>('/api/sourcegraph.configuration', {}) - .pipe(map(({ url }) => url)) - .toPromise() + return lastValueFrom( + queryConduitHelper<{ url: string }>('/api/sourcegraph.configuration', {}).pipe(map(({ url }) => url)) + ) } const getRepoDetailsFromRepoPHID = memoizeObservable( diff --git a/client/browser/src/shared/code-hosts/phabricator/fileInfo.test.ts b/client/browser/src/shared/code-hosts/phabricator/fileInfo.test.ts index 0ce991d0f77..ae5b1ee1910 100644 --- a/client/browser/src/shared/code-hosts/phabricator/fileInfo.test.ts +++ b/client/browser/src/shared/code-hosts/phabricator/fileInfo.test.ts @@ -1,5 +1,5 @@ import { readFile } from 'mz/fs' -import { type Observable, throwError, of } from 'rxjs' +import { type Observable, throwError, of, lastValueFrom } from 'rxjs' import { beforeEach, describe, expect, test } from 'vitest' import { resetAllMemoizationCaches } from '@sourcegraph/common' @@ -145,15 +145,17 @@ const resolveFileInfoFromFixture = async ( if (!codeView) { throw new Error(`Code view matching selector ${codeViewSelector} not found`) } - return resolver( - codeView as HTMLElement, - mockRequestGraphQL({ - ...DEFAULT_GRAPHQL_RESPONSES, - ...graphQLResponseMap, - }), - mockQueryConduit(conduitResponseMap), - new URL(url) - ).toPromise() + return lastValueFrom( + resolver( + codeView as HTMLElement, + mockRequestGraphQL({ + ...DEFAULT_GRAPHQL_RESPONSES, + ...graphQLResponseMap, + }), + mockQueryConduit(conduitResponseMap), + new URL(url) + ) + ) } describe('Phabricator file info', () => { diff --git a/client/browser/src/shared/code-hosts/shared/codeHost.tsx b/client/browser/src/shared/code-hosts/shared/codeHost.tsx index 32f2a42198c..75809864a92 100644 --- a/client/browser/src/shared/code-hosts/shared/codeHost.tsx +++ b/client/browser/src/shared/code-hosts/shared/codeHost.tsx @@ -18,6 +18,7 @@ import { concat, BehaviorSubject, fromEvent, + lastValueFrom, } from 'rxjs' import { catchError, @@ -643,10 +644,12 @@ const isSafeToContinueCodeIntel = async ({ rawRepoName = context.rawRepoName - const isRepoCloned = await resolvePrivateRepo({ - rawRepoName, - requestGraphQL, - }).toPromise() + const isRepoCloned = await lastValueFrom( + resolvePrivateRepo({ + rawRepoName, + requestGraphQL, + }) + ) return isRepoCloned } catch (error) { diff --git a/client/browser/src/shared/code-hosts/shared/codeViews.test.ts b/client/browser/src/shared/code-hosts/shared/codeViews.test.ts index a66b8dfd844..7e5249e1df4 100644 --- a/client/browser/src/shared/code-hosts/shared/codeViews.test.ts +++ b/client/browser/src/shared/code-hosts/shared/codeViews.test.ts @@ -1,4 +1,4 @@ -import { of } from 'rxjs' +import { lastValueFrom, of } from 'rxjs' import { toArray } from 'rxjs/operators' import * as sinon from 'sinon' import type { Omit } from 'utility-types' @@ -33,14 +33,14 @@ describe('codeViews', () => { element.className = 'test-code-view' document.body.append(element) const selector = '.test-code-view' - const detected = await of([{ addedNodes: [document.body], removedNodes: [] }]) - .pipe( + const detected = await lastValueFrom( + of([{ addedNodes: [document.body], removedNodes: [] }]).pipe( trackCodeViews({ codeViewResolvers: [toCodeViewResolver(selector, codeViewSpec)], }), toArray() ) - .toPromise() + ) expect(detected.map(({ subscriptions, ...rest }) => rest)).toEqual([{ ...codeViewSpec, element }]) }) it('should detect added code views from resolver', async () => { @@ -49,14 +49,14 @@ describe('codeViews', () => { document.body.append(element) const selector = '.test-code-view' const resolveView = sinon.spy((element: HTMLElement) => ({ element, ...codeViewSpec })) - const detected = await of([{ addedNodes: [document.body], removedNodes: [] }]) - .pipe( + const detected = await lastValueFrom( + of([{ addedNodes: [document.body], removedNodes: [] }]).pipe( trackCodeViews({ codeViewResolvers: [{ selector, resolveView }], }), toArray() ) - .toPromise() + ) expect(detected.map(({ subscriptions, ...rest }) => rest)).toEqual([{ ...codeViewSpec, element }]) sinon.assert.calledOnce(resolveView) sinon.assert.calledWith(resolveView, element) diff --git a/client/browser/src/shared/code-hosts/shared/views.test.ts b/client/browser/src/shared/code-hosts/shared/views.test.ts index afa64df04e6..2bd7bf32b15 100644 --- a/client/browser/src/shared/code-hosts/shared/views.test.ts +++ b/client/browser/src/shared/code-hosts/shared/views.test.ts @@ -1,5 +1,5 @@ import { noop } from 'lodash' -import { from, type Observable, of, Subject, Subscription, NEVER } from 'rxjs' +import { from, type Observable, of, Subject, Subscription, NEVER, lastValueFrom } from 'rxjs' import { bufferCount, map, switchMap, toArray } from 'rxjs/operators' import * as sinon from 'sinon' import { afterAll, beforeEach, describe, expect, test } from 'vitest' @@ -40,9 +40,9 @@ describe('trackViews()', () => { test('detects all views on the page', async () => { const mutations: Observable = of([{ addedNodes: [document.body], removedNodes: [] }]) - const views = await mutations - .pipe(trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), toArray()) - .toPromise() + const views = await lastValueFrom( + mutations.pipe(trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), toArray()) + ) expect(views.map(({ element }) => element.id)).toEqual(['view1', 'view2', 'view3']) }) @@ -51,13 +51,13 @@ describe('trackViews()', () => { { addedNodes: [document.querySelector('#view1')!], removedNodes: [] }, ]) expect( - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), map(({ element }) => element.id), toArray() ) - .toPromise() + ) ).toEqual(['view1']) }) @@ -66,13 +66,13 @@ describe('trackViews()', () => { { addedNodes: [document.querySelector('#view1')!], removedNodes: [] }, ]) expect( - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), map(({ element }) => element.id), toArray() ) - .toPromise() + ) ).toEqual(['view1']) }) @@ -82,8 +82,8 @@ describe('trackViews()', () => { selectorTarget.className = 'selector-target' document.querySelector('#view1')!.append(selectorTarget) expect( - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([ { selector: '.selector-target', @@ -93,15 +93,15 @@ describe('trackViews()', () => { map(({ element }) => element.id), toArray() ) - .toPromise() + ) ).toEqual(['view1']) }) test("doesn't emit duplicate views", async () => { const mutations: Observable = of([{ addedNodes: [document.body], removedNodes: [] }]) expect( - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([ { selector: '.view', @@ -113,7 +113,7 @@ describe('trackViews()', () => { map(({ element }) => element.id), toArray() ) - .toPromise() + ) ).toEqual(['view1']) }) @@ -190,20 +190,20 @@ describe('trackViews()', () => { [{ addedNodes: [], removedNodes: [document.querySelector('#view1')!] }], [{ addedNodes: [], removedNodes: [document.querySelector('#view3')!] }], ]) - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), bufferCount(3), switchMap(async ([view1, view2, view3]) => { const v2Removed = sinon.spy(() => undefined) view2.subscriptions.add(v2Removed) - const v1Removed = new Promise(resolve => view1.subscriptions.add(resolve)) - const v3Removed = new Promise(resolve => view3.subscriptions.add(resolve)) + const v1Removed = new Promise(resolve => view1.subscriptions.add(resolve)) + const v3Removed = new Promise(resolve => view3.subscriptions.add(resolve)) await Promise.all([v1Removed, v3Removed]) sinon.assert.notCalled(v2Removed) }) ) - .toPromise() + ) }) test('removes all nested views', async () => { @@ -211,15 +211,15 @@ describe('trackViews()', () => { [{ addedNodes: [document.body], removedNodes: [] }], [{ addedNodes: [], removedNodes: [document.querySelector('#parent')!] }], ]) - await mutations - .pipe( + await lastValueFrom( + mutations.pipe( trackViews([{ selector: '.view', resolveView: element => ({ element }) }]), bufferCount(3), switchMap(views => - Promise.all(views.map(view => new Promise(resolve => view.subscriptions.add(resolve)))) + Promise.all(views.map(view => new Promise(resolve => view.subscriptions.add(resolve)))) ) ) - .toPromise() + ) }) test('removes a view without depending on its resolver', async () => { @@ -259,7 +259,7 @@ describe('trackViews()', () => { expect(resolver.resolveView(testElement)).toBe(null) // Verify that the code view still gets removed. - const unsubscribed = new Promise(resolve => view.subscriptions.add(resolve)) + const unsubscribed = new Promise(resolve => view.subscriptions.add(resolve)) mutations.next([{ addedNodes: [], removedNodes: [testElement] }]) await unsubscribed }) diff --git a/client/browser/src/shared/extensionHostWorker.ts b/client/browser/src/shared/extensionHostWorker.ts index 1c6862f9bad..35ccce4478b 100644 --- a/client/browser/src/shared/extensionHostWorker.ts +++ b/client/browser/src/shared/extensionHostWorker.ts @@ -1,7 +1,6 @@ import '@sourcegraph/shared/src/polyfills' -import { fromEvent } from 'rxjs' -import { take } from 'rxjs/operators' +import { firstValueFrom, fromEvent } from 'rxjs' import { hasProperty, logger } from '@sourcegraph/common' import { startExtensionHost } from '@sourcegraph/shared/src/api/extension/extensionHost' @@ -24,7 +23,7 @@ const isInitMessage = (value: unknown): value is InitMessage => */ async function extensionHostMain(): Promise { try { - const event = await fromEvent(self, 'message').pipe(take(1)).toPromise() + const event = await firstValueFrom(fromEvent(self, 'message')) if (!isInitMessage(event.data)) { throw new Error('First message event in extension host worker was not a well-formed InitMessage') } diff --git a/client/browser/src/shared/platform/context.ts b/client/browser/src/shared/platform/context.ts index 8a526d2b820..68bc573bcfa 100644 --- a/client/browser/src/shared/platform/context.ts +++ b/client/browser/src/shared/platform/context.ts @@ -1,4 +1,4 @@ -import { combineLatest, ReplaySubject } from 'rxjs' +import { combineLatest, lastValueFrom, ReplaySubject } from 'rxjs' import { map } from 'rxjs/operators' import { asError } from '@sourcegraph/common' @@ -77,7 +77,7 @@ export function createPlatformContext( ), refreshSettings: async () => { try { - const settings = await fetchViewerSettings(requestGraphQL).toPromise() + const settings = await lastValueFrom(fetchViewerSettings(requestGraphQL)) updatedViewerSettings.next(settings) } catch (error) { if (isHTTPAuthError(error)) { diff --git a/client/build-config/src/esbuild/packageResolutionPlugin.ts b/client/build-config/src/esbuild/packageResolutionPlugin.ts index 77041d4f03a..68c1207702e 100644 --- a/client/build-config/src/esbuild/packageResolutionPlugin.ts +++ b/client/build-config/src/esbuild/packageResolutionPlugin.ts @@ -51,14 +51,3 @@ export const packageResolutionPlugin = (resolutions: Resolutions): esbuild.Plugi build.onLoad({ filter: new RegExp(''), namespace: 'devnull' }, () => ({ contents: '' })) }, }) - -export const RXJS_RESOLUTIONS: Resolutions = { - // Needed because imports of rxjs/internal/... actually import a different variant of - // rxjs in the same package, which leads to observables from combineLatestOrDefault (and - // other places that use rxjs/internal/...) not being cross-compatible. See - // https://stackoverflow.com/questions/53758889/rxjs-subscribeto-js-observable-check-works-in-chrome-but-fails-in-chrome-incogn. - 'rxjs/internal/OuterSubscriber': require.resolve('rxjs/_esm5/internal/OuterSubscriber'), - 'rxjs/internal/util/subscribeToResult': require.resolve('rxjs/_esm5/internal/util/subscribeToResult'), - 'rxjs/internal/util/subscribeToArray': require.resolve('rxjs/_esm5/internal/util/subscribeToArray'), - 'rxjs/internal/Observable': require.resolve('rxjs/_esm5/internal/Observable'), -} diff --git a/client/build-config/src/esbuild/workerPlugin.ts b/client/build-config/src/esbuild/workerPlugin.ts index 99cb4e64a01..969ab813f9e 100644 --- a/client/build-config/src/esbuild/workerPlugin.ts +++ b/client/build-config/src/esbuild/workerPlugin.ts @@ -1,6 +1,6 @@ import type * as esbuild from 'esbuild' -import { packageResolutionPlugin, RXJS_RESOLUTIONS } from './packageResolutionPlugin' +import { packageResolutionPlugin } from './packageResolutionPlugin' /** * Starts a new esbuild build to create a bundle for a Web Worker. @@ -21,7 +21,6 @@ async function buildWorker( plugins: [ packageResolutionPlugin({ path: require.resolve('path-browserify'), - ...RXJS_RESOLUTIONS, }), ], // Use the minify option as an indicator for running in dev mode. diff --git a/client/codeintellify/src/helpers.ts b/client/codeintellify/src/helpers.ts index 03b2dfd7edd..d201f213ccf 100644 --- a/client/codeintellify/src/helpers.ts +++ b/client/codeintellify/src/helpers.ts @@ -1,5 +1,5 @@ import { isObject } from 'lodash' -import { type Subscribable, type Observable, from } from 'rxjs' +import { type Observable, from } from 'rxjs' import { map } from 'rxjs/operators' import { isDefined } from '@sourcegraph/common' @@ -17,9 +17,9 @@ const isPromiseLike = (value: unknown): value is PromiseLike => * single result, to the same type. */ export const toMaybeLoadingProviderResult = ( - value: Subscribable> | PromiseLike + value: Observable> | PromiseLike ): Observable> => - isPromiseLike(value) ? from(value).pipe(map(result => ({ isLoading: false, result }))) : from(value) + isPromiseLike(value) ? from(value).pipe(map(result => ({ isLoading: false, result }))) : value /** * Returns a function that returns `true` if the given `key` of the object is not `null` or `undefined`. diff --git a/client/codeintellify/src/hoverifier.test.ts b/client/codeintellify/src/hoverifier.test.ts index 938a839e5d1..57e7fa507f1 100644 --- a/client/codeintellify/src/hoverifier.test.ts +++ b/client/codeintellify/src/hoverifier.test.ts @@ -2,7 +2,7 @@ import { isEqual } from 'lodash' import { EMPTY, NEVER, of, Subject, Subscription } from 'rxjs' import { delay, distinctUntilChanged, filter, first, map, takeWhile } from 'rxjs/operators' import { TestScheduler } from 'rxjs/testing' -import { afterAll, afterEach, beforeAll, describe, it, expect } from 'vitest' +import { afterEach, beforeEach, describe, it, expect } from 'vitest' import { isDefined } from '@sourcegraph/common' import type { Range } from '@sourcegraph/extension-api-types' @@ -27,17 +27,18 @@ import { import { dispatchMouseEventAtPositionImpure } from './testutils/mouse' describe('Hoverifier', () => { - const dom = new DOM() - afterAll(dom.cleanup) - + let dom: DOM let testcases: CodeViewProps[] = [] - beforeAll(() => { + + beforeEach(() => { + dom = new DOM() testcases = dom.createCodeViews() }) let subscriptions = new Subscription() afterEach(() => { + dom.cleanup() subscriptions.unsubscribe() subscriptions = new Subscription() }) diff --git a/client/codeintellify/src/hoverifier.ts b/client/codeintellify/src/hoverifier.ts index 2f9c6a3aaae..2a2be5daa22 100644 --- a/client/codeintellify/src/hoverifier.ts +++ b/client/codeintellify/src/hoverifier.ts @@ -11,11 +11,10 @@ import { type Observable, of, Subject, - type Subscribable, - type SubscribableOrPromise, Subscription, race, type MonoTypeOperatorFunction, + ObservableInput, } from 'rxjs' import { catchError, @@ -75,7 +74,7 @@ export interface HoverifierOptions { /** * Emit the HoverOverlay element on this after it was rerendered when its content changed and it needs to be repositioned. */ - hoverOverlayRerenders: Subscribable<{ + hoverOverlayRerenders: ObservableInput<{ /** * The HoverOverlay element */ @@ -89,13 +88,13 @@ export interface HoverifierOptions { pinOptions?: { /** Emit on this Observable to pin the popover. */ - pins: Subscribable + pins: ObservableInput /** * Emit on this Observable when the close button in the HoverOverlay was clicked */ - closeButtonClicks: Subscribable + closeButtonClicks: ObservableInput } - hoverOverlayElements: Subscribable + hoverOverlayElements: ObservableInput /** * Called to get the data to display in the hover. @@ -199,7 +198,7 @@ export interface AdjustPositionProps { * * @template C Extra context for the hovered token. */ -export type PositionAdjuster = (props: AdjustPositionProps) => SubscribableOrPromise +export type PositionAdjuster = (props: AdjustPositionProps) => ObservableInput /** * HoverifyOptions that need to be included internally with every event @@ -235,13 +234,13 @@ export interface EventOptions { */ export interface HoverifyOptions extends Pick, Exclude, 'codeViewId'>> { - positionEvents: Subscribable + positionEvents: ObservableInput /** * Emit on this Observable to trigger the overlay on a position in this code view. * This Observable is intended to be used to trigger a Hover after a URL change with a position. */ - positionJumps?: Subscribable + positionJumps?: ObservableInput } /** @@ -388,7 +387,7 @@ export const MOUSEOVER_DELAY = 50 */ export type HoverProvider = ( position: HoveredToken & C -) => Subscribable> | PromiseLike<(HoverAttachment & D) | null> +) => Observable> | PromiseLike<(HoverAttachment & D) | null> /** * Function that returns a Subscribable or PromiseLike of the ranges to be highlighted in the document. @@ -399,13 +398,13 @@ export type HoverProvider = ( */ export type DocumentHighlightProvider = ( position: HoveredToken & C -) => Subscribable | PromiseLike +) => ObservableInput /** * @template C Extra context for the hovered token. * @template A The type of an action. */ -export type ActionsProvider = (position: HoveredToken & C) => SubscribableOrPromise +export type ActionsProvider = (position: HoveredToken & C) => ObservableInput /** * Function responsible for resolving the position of a hovered token @@ -1050,7 +1049,9 @@ export function createHoverifier({ } // Pin on request. - subscription.add(pinOptions?.pins.subscribe(() => container.update({ pinned: true }))) + if (pinOptions) { + subscription.add(from(pinOptions.pins).subscribe(() => container.update({ pinned: true }))) + } // Unpin on close, ESC, or click. subscription.add( diff --git a/client/codeintellify/src/positions.ts b/client/codeintellify/src/positions.ts index 4dd53a5cf4d..5f071787fc0 100644 --- a/client/codeintellify/src/positions.ts +++ b/client/codeintellify/src/positions.ts @@ -1,4 +1,4 @@ -import { from, fromEvent, merge, type Observable, type Subscribable } from 'rxjs' +import { from, fromEvent, merge, type Observable } from 'rxjs' import { filter, map, switchMap, tap } from 'rxjs/operators' import { convertCodeElementIdempotent, type DOMFunctions, type HoveredToken, locateTarget } from './tokenPosition' @@ -28,7 +28,7 @@ export interface PositionEvent { export const findPositionsFromEvents = ({ domFunctions, tokenize = true }: { domFunctions: DOMFunctions; tokenize?: boolean }) => - (elements: Subscribable): Observable => + (elements: Observable): Observable => merge( from(elements).pipe( switchMap(element => diff --git a/client/codeintellify/src/testutils/fixtures.ts b/client/codeintellify/src/testutils/fixtures.ts index 7cb08ad455c..6b24bc8a76c 100644 --- a/client/codeintellify/src/testutils/fixtures.ts +++ b/client/codeintellify/src/testutils/fixtures.ts @@ -2,7 +2,6 @@ import { of } from 'rxjs' import { delay } from 'rxjs/operators' import type { ActionsProvider, HoverProvider, DocumentHighlightProvider } from '../hoverifier' -import type { MaybeLoadingResult } from '../loading' import type { HoverAttachment, DocumentHighlight } from '../types' /** @@ -41,10 +40,7 @@ export function createStubHoverProvider( hover: Partial = {}, delayTime?: number ): HoverProvider<{}, {}> { - return () => - of>({ isLoading: false, result: createHoverAttachment(hover) }).pipe( - delay(delayTime ?? 0) - ) + return () => of({ isLoading: false, result: createHoverAttachment(hover) }).pipe(delay(delayTime ?? 0)) } /** @@ -57,7 +53,7 @@ export function createStubDocumentHighlightProvider( documentHighlights: Partial[] = [], delayTime?: number ): DocumentHighlightProvider<{}> { - return () => of(documentHighlights.map(createDocumentHighlight)).pipe(delay(delayTime ?? 0)) + return () => of(documentHighlights.map(createDocumentHighlight)).pipe(delay(delayTime ?? 0)) } /** diff --git a/client/common/BUILD.bazel b/client/common/BUILD.bazel index 903146a63d2..a471484396e 100644 --- a/client/common/BUILD.bazel +++ b/client/common/BUILD.bazel @@ -46,6 +46,7 @@ ts_project( "src/util/path.ts", "src/util/rxjs/asObservable.ts", "src/util/rxjs/combineLatestOrDefault.ts", + "src/util/rxjs/fromSubscribable.ts", "src/util/rxjs/index.ts", "src/util/rxjs/memoizeObservable.ts", "src/util/rxjs/repeatUntil.ts", @@ -71,6 +72,7 @@ ts_project( "//:node_modules/marked", "//:node_modules/react-router-dom", "//:node_modules/rxjs", + "//:node_modules/sourcegraph", "//:node_modules/utility-types", ], ) diff --git a/client/common/src/util/rxjs/combineLatestOrDefault.ts b/client/common/src/util/rxjs/combineLatestOrDefault.ts index 389182d071b..5ed68f70f74 100644 --- a/client/common/src/util/rxjs/combineLatestOrDefault.ts +++ b/client/common/src/util/rxjs/combineLatestOrDefault.ts @@ -1,18 +1,4 @@ -/* eslint rxjs/no-internal: warn */ -import { - asapScheduler, - type ObservableInput, - of, - type Operator, - type PartialObserver, - type Subscriber, - type TeardownLogic, - zip, -} from 'rxjs' -import { Observable } from 'rxjs/internal/Observable' -import { OuterSubscriber } from 'rxjs/internal/OuterSubscriber' -import { subscribeToArray } from 'rxjs/internal/util/subscribeToArray' -import { subscribeToResult } from 'rxjs/internal/util/subscribeToResult' +import { asapScheduler, type ObservableInput, Observable, of, zip, from, Subscription } from 'rxjs' /** * Like {@link combineLatest}, except that it does not wait for all Observables to emit before emitting an initial @@ -51,74 +37,68 @@ export function combineLatestOrDefault(observables: ObservableInput[], def return zip(...observables) } default: { - return new Observable(subscribeToArray(observables)).lift(new CombineLatestOperator(defaultValue)) - } - } -} + return new Observable(subscriber => { + // The array of the most recent values from each input Observable. + // If a source Observable has not yet emitted a value, it will be represented by the + // defaultValue (if provided) or not at all (if not provided). + const values: T[] = defaultValue !== undefined ? observables.map(() => defaultValue) : [] -class CombineLatestOperator implements Operator { - constructor(private defaultValue?: T) {} + // Whether the emission of the values array has been scheduled + let scheduled = false + let scheduledWork: Subscription | undefined + // The number of source Observables that have not yet completed + // (so that we know when to complete the output Observable) + let activeObservables = observables.length - public call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new CombineLatestSubscriber(subscriber, this.defaultValue)) - } -} + // When everything is done, clean up the values array + subscriber.add(() => { + values.length = 0 + }) -class CombineLatestSubscriber extends OuterSubscriber { - private activeObservables = 0 - private values: any[] = [] - private observables: Observable[] = [] - private scheduled = false - - constructor(observer: PartialObserver, private defaultValue?: T) { - super(observer) - } - - protected _next(observable: any): void { - if (this.defaultValue !== undefined) { - this.values.push(this.defaultValue) - } - this.observables.push(observable) - } - - protected _complete(): void { - this.activeObservables = this.observables.length - for (let index = 0; index < this.observables.length; index++) { - this.add(subscribeToResult(this, this.observables[index], this.observables[index], index)) - } - } - - public notifyComplete(): void { - this.activeObservables-- - if (this.activeObservables === 0 && this.destination.complete) { - this.destination.complete() - } - } - - public notifyNext(_outerValue: T, innerValue: T[], outerIndex: number): void { - const values = this.values - values[outerIndex] = innerValue - - if (this.activeObservables === 1) { - // Only 1 observable is active, so no need to buffer. - // - // This makes it possible to use RxJS's `of` in tests without specifying an explicit scheduler. - if (this.destination.next) { - this.destination.next(this.values.slice()) - } - return - } - - // Buffer all next values that are emitted at the same time into one emission. - // - // This makes tests (using expectObservable) easier to write. - if (!this.scheduled) { - this.scheduled = true - asapScheduler.schedule(() => { - if (this.scheduled && this.destination.next) { - this.destination.next(this.values.slice()) + // Subscribe to each source Observable. The index of the source Observable is used to + // keep track of the most recent value from that Observable in the values array. + for (let index = 0; index < observables.length; index++) { + subscriber.add( + from(observables[index]).subscribe({ + next: value => { + values[index] = value + if (activeObservables === 1) { + // If only one source Observable is active, emit the values array immediately + // Abort any scheduled emission + scheduledWork?.unsubscribe() + scheduled = false + subscriber.next(values.slice()) + } else if (!scheduled) { + scheduled = true + // Use asapScheduler to emit the values array, so that all + // next values that are emitted at the same time are emitted together. + // This makes tests (using expectObservable) easier to write. + scheduledWork = asapScheduler.schedule(() => { + if (!subscriber.closed) { + subscriber.next(values.slice()) + scheduled = false + if (activeObservables === 0) { + subscriber.complete() + } + } + }) + } + }, + error: error => subscriber.error(error), + complete: () => { + activeObservables-- + if (activeObservables === 0 && !scheduled) { + subscriber.complete() + } + }, + }) + ) + } + + // When everything is done, clean up the values array + return () => { + values.length = 0 } - this.scheduled = false }) } } diff --git a/client/common/src/util/rxjs/fromSubscribable.ts b/client/common/src/util/rxjs/fromSubscribable.ts new file mode 100644 index 00000000000..4e60bcb5c20 --- /dev/null +++ b/client/common/src/util/rxjs/fromSubscribable.ts @@ -0,0 +1,14 @@ +import { Observable, isObservable } from 'rxjs' +import { type Subscribable } from 'sourcegraph' + +/** + * Converts a Sourcegraph {@link Subscribable} to an {@link Observable}. + */ +export function fromSubscribable(value: Subscribable): Observable { + if (isObservable(value)) { + // type casting should be fine since we already know that + // value is at least a Subscribable + return value as Observable + } + return new Observable(subscriber => value.subscribe(subscriber)) +} diff --git a/client/common/src/util/rxjs/index.ts b/client/common/src/util/rxjs/index.ts index 89f4fcd4754..e33b08dc252 100644 --- a/client/common/src/util/rxjs/index.ts +++ b/client/common/src/util/rxjs/index.ts @@ -2,3 +2,4 @@ export * from './asObservable' export * from './combineLatestOrDefault' export * from './memoizeObservable' export * from './repeatUntil' +export * from './fromSubscribable' diff --git a/client/jetbrains/webview/src/sourcegraph-api-access/api-gateway.ts b/client/jetbrains/webview/src/sourcegraph-api-access/api-gateway.ts index 5c259992448..e6ffd245a0a 100644 --- a/client/jetbrains/webview/src/sourcegraph-api-access/api-gateway.ts +++ b/client/jetbrains/webview/src/sourcegraph-api-access/api-gateway.ts @@ -1,3 +1,5 @@ +import { lastValueFrom } from 'rxjs' + import { gql, requestGraphQLCommon } from '@sourcegraph/http-client' import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import type { CurrentAuthStateResult, CurrentAuthStateVariables } from '@sourcegraph/shared/src/graphql-operations' @@ -57,18 +59,20 @@ export async function getSiteVersionAndAuthenticatedUser( return { site: null, currentUser: null } } - const result = await requestGraphQLCommon({ - request: siteVersionAndUserQuery, - variables: {}, - baseUrl: instanceURL, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false', - ...(accessToken && { Authorization: `token ${accessToken}` }), - ...customRequestHeaders, - }, - }).toPromise() + const result = await lastValueFrom( + requestGraphQLCommon({ + request: siteVersionAndUserQuery, + variables: {}, + baseUrl: instanceURL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false', + ...(accessToken && { Authorization: `token ${accessToken}` }), + ...customRequestHeaders, + }, + }) + ) return result.data ?? { site: null, currentUser: null } } diff --git a/client/shared/src/actions/ActionsNavItems.tsx b/client/shared/src/actions/ActionsNavItems.tsx index b2873749903..f1a644e07a8 100644 --- a/client/shared/src/actions/ActionsNavItems.tsx +++ b/client/shared/src/actions/ActionsNavItems.tsx @@ -82,12 +82,12 @@ export interface ActionsNavItemsProps export const ActionsNavItems: React.FunctionComponent> = props => { const { scope, extraContext, extensionsController, menu, wrapInList, transformContributions = identity } = props - const scopeChanges = useMemo(() => new ReplaySubject(1), []) + const scopeChanges = useMemo(() => new ReplaySubject(1), []) useDeepCompareEffectNoCheck(() => { scopeChanges.next(scope) }, [scope]) - const extraContextChanges = useMemo(() => new ReplaySubject>(1), []) + const extraContextChanges = useMemo(() => new ReplaySubject | undefined>(1), []) useDeepCompareEffectNoCheck(() => { extraContextChanges.next(extraContext) }, [extraContext]) diff --git a/client/shared/src/api/client/api/common.ts b/client/shared/src/api/client/api/common.ts index 2564dfe516d..6a3b4c91997 100644 --- a/client/shared/src/api/client/api/common.ts +++ b/client/shared/src/api/client/api/common.ts @@ -1,6 +1,5 @@ import { type Remote, proxyMarker, releaseProxy, type ProxyMethods, type ProxyOrClone } from 'comlink' -import { noop } from 'lodash' -import { from, type Observable, observable as symbolObservable, Subscription, type Subscribable } from 'rxjs' +import { from, Observable, Subscription } from 'rxjs' import { mergeMap, finalize } from 'rxjs/operators' import { asError, logger } from '@sourcegraph/common' @@ -59,37 +58,18 @@ export const wrapRemoteObservable = ( const observable = from( isPromiseLike(proxyOrProxyPromise) ? proxyOrProxyPromise : Promise.resolve(proxyOrProxyPromise) ).pipe( - mergeMap((proxySubscribable): Subscribable> => { + mergeMap((proxySubscribable): Observable> => { proxySubscription.add(new ProxySubscription(proxySubscribable)) - return { - // Needed for Rx type check - [symbolObservable](): Subscribable> { - return this - }, - subscribe(...args: any[]): Subscription { - // Always subscribe with an object because the other side - // is unable to tell if a Proxy is a function or an observer object - // (they always appear as functions) - let proxyObserver: Parameters[0] - if (typeof args[0] === 'function') { - proxyObserver = { - [proxyMarker]: true, - next: args[0] || noop, - error: args[1] ? error => args[1](asError(error)) : noop, - complete: args[2] || noop, - } - } else { - const partialObserver = args[0] || {} - proxyObserver = { - [proxyMarker]: true, - next: partialObserver.next ? value => partialObserver.next(value) : noop, - error: partialObserver.error ? error => partialObserver.error(asError(error)) : noop, - complete: partialObserver.complete ? () => partialObserver.complete() : noop, - } - } - return syncRemoteSubscription(proxySubscribable.subscribe(proxyObserver)) - }, - } + return new Observable(subscriber => { + const proxyObserver: Parameters[0] = { + [proxyMarker]: true, + // @ts-expect-error - this was previously typed as any + next: value => subscriber.next(value), + error: error => subscriber.error(asError(error)), + complete: () => subscriber.complete(), + } + return syncRemoteSubscription(proxySubscribable.subscribe(proxyObserver)) + }) }) ) return Object.assign(observable, { proxySubscription }) diff --git a/client/shared/src/api/client/connection.ts b/client/shared/src/api/client/connection.ts index c60c64edf03..cc119403a12 100644 --- a/client/shared/src/api/client/connection.ts +++ b/client/shared/src/api/client/connection.ts @@ -1,6 +1,5 @@ import * as comlink from 'comlink' -import { from, Subscription, type Unsubscribable } from 'rxjs' -import { first } from 'rxjs/operators' +import { firstValueFrom, Subscription, type Unsubscribable } from 'rxjs' import { logger } from '@sourcegraph/common' @@ -42,7 +41,7 @@ export async function createExtensionHostClientConnection( /** Proxy to the exposed extension host API */ const initializeExtensionHost = comlink.wrap(endpoints.proxy) - const initialSettings = await from(platformContext.settings).pipe(first()).toPromise() + const initialSettings = await firstValueFrom(platformContext.settings) const proxy = await initializeExtensionHost({ ...initData, // TODO what to do in error case? diff --git a/client/shared/src/api/client/mainthread-api.test.ts b/client/shared/src/api/client/mainthread-api.test.ts index 7eae423c44f..7d238751722 100644 --- a/client/shared/src/api/client/mainthread-api.test.ts +++ b/client/shared/src/api/client/mainthread-api.test.ts @@ -19,7 +19,7 @@ describe('MainThreadAPI', () => { describe('graphQL', () => { test('PlatformContext#requestGraphQL is called with the correct arguments', async () => { - const requestGraphQL = sinon.spy(_options => EMPTY) + const requestGraphQL = sinon.spy(_options => of({ data: null, errors: [] })) const platformContext: Pick< PlatformContext, diff --git a/client/shared/src/api/client/mainthread-api.ts b/client/shared/src/api/client/mainthread-api.ts index 5b7e08a84f6..7b22c17a87e 100644 --- a/client/shared/src/api/client/mainthread-api.ts +++ b/client/shared/src/api/client/mainthread-api.ts @@ -1,5 +1,5 @@ import { type Remote, proxy } from 'comlink' -import { type Unsubscribable, Subscription, from, of } from 'rxjs' +import { type Unsubscribable, Subscription, from, of, lastValueFrom } from 'rxjs' import { publishReplay, refCount, switchMap } from 'rxjs/operators' import { logger } from '@sourcegraph/common' @@ -101,13 +101,13 @@ export const initMainThreadAPI = ( const api: MainThreadAPI = { applySettingsEdit: edit => updateSettings(platformContext, edit), requestGraphQL: (request, variables) => - platformContext - .requestGraphQL({ + lastValueFrom( + platformContext.requestGraphQL({ request, variables, mightContainPrivateInfo: true, }) - .toPromise(), + ), // Commands executeCommand: (command, args) => executeCommand({ command, args }), registerCommand: (command, run) => { diff --git a/client/shared/src/api/client/services/settings.ts b/client/shared/src/api/client/services/settings.ts index b56a7c3803f..a252915cb06 100644 --- a/client/shared/src/api/client/services/settings.ts +++ b/client/shared/src/api/client/services/settings.ts @@ -1,5 +1,4 @@ -import { from } from 'rxjs' -import { first } from 'rxjs/operators' +import { firstValueFrom } from 'rxjs' import type { KeyPath } from '@sourcegraph/client-api' @@ -28,7 +27,7 @@ export async function updateSettings( edit: SettingsEdit ): Promise { const { settings: data, updateSettings: update } = platformContext - const settings = await from(data).pipe(first()).toPromise() + const settings = await firstValueFrom(data) if (!isSettingsValid(settings)) { throw new Error('invalid settings (internal error)') } diff --git a/client/shared/src/api/contract.ts b/client/shared/src/api/contract.ts index ef825b50f2a..e5d7ad1d668 100644 --- a/client/shared/src/api/contract.ts +++ b/client/shared/src/api/contract.ts @@ -1,6 +1,5 @@ import type { Remote, ProxyMarked } from 'comlink' import type { Unsubscribable } from 'rxjs' -import type { DocumentHighlight } from 'sourcegraph' import type { Contributions, @@ -13,7 +12,7 @@ import type { MaybeLoadingResult } from '@sourcegraph/codeintellify' import type * as clientType from '@sourcegraph/extension-api-types' import type { GraphQLResult } from '@sourcegraph/http-client' -import type { ReferenceContext } from '../codeintel/legacy-extensions/api' +import type { DocumentHighlight, ReferenceContext } from '../codeintel/legacy-extensions/api' import type { Occurrence } from '../codeintel/scip' import type { ConfiguredExtension } from '../extensions/extension' import type { SettingsCascade } from '../settings/settings' diff --git a/client/shared/src/api/extension/api/common.ts b/client/shared/src/api/extension/api/common.ts index 6f4c86a04e7..3518d5bdd35 100644 --- a/client/shared/src/api/extension/api/common.ts +++ b/client/shared/src/api/extension/api/common.ts @@ -1,8 +1,10 @@ import { type Remote, type ProxyMarked, proxy, proxyMarker, type UnproxyOrClone } from 'comlink' import { identity } from 'lodash' -import { from, isObservable, type Observable, type Observer, of, type Subscribable, type Unsubscribable } from 'rxjs' +import { from, isObservable, type Observable, type Observer, of, type Unsubscribable, ObservableInput } from 'rxjs' import { map } from 'rxjs/operators' -import type { ProviderResult } from 'sourcegraph' +import type { ProviderResult, Subscribable } from 'sourcegraph' + +import { fromSubscribable } from '@sourcegraph/common' import { isAsyncIterable, isPromiseLike, isSubscribable, observableFromAsyncIterable } from '../../util' @@ -50,8 +52,10 @@ export function providerResultToObservable( mapFunc: (value: T | undefined | null) => R = identity ): Observable { let observable: Observable - if (result && (isPromiseLike(result) || isObservable(result) || isSubscribable(result))) { - observable = from(result).pipe(map(mapFunc)) + if (result && (isPromiseLike(result) || isObservable(result))) { + observable = from(result as ObservableInput).pipe(map(mapFunc)) + } else if (isSubscribable(result)) { + observable = fromSubscribable(result as Subscribable).pipe(map(mapFunc)) } else if (isAsyncIterable(result)) { observable = observableFromAsyncIterable(result).pipe(map(mapFunc)) } else { diff --git a/client/shared/src/api/extension/extensionHostApi.ts b/client/shared/src/api/extension/extensionHostApi.ts index a0cde462607..6416b91cf08 100644 --- a/client/shared/src/api/extension/extensionHostApi.ts +++ b/client/shared/src/api/extension/extensionHostApi.ts @@ -1,6 +1,6 @@ import { proxy } from 'comlink' import { castArray, isEqual } from 'lodash' -import { combineLatest, concat, type Observable, of, type Subscribable } from 'rxjs' +import { combineLatest, concat, type Observable, of } from 'rxjs' import { catchError, defaultIfEmpty, distinctUntilChanged, map, switchMap } from 'rxjs/operators' import type { ProviderResult } from 'sourcegraph' @@ -319,7 +319,7 @@ export function createExtensionHostAPI(state: ExtensionHostState): FlatExtension }) ), state.settings, - state.context as Subscribable>, + state.context as Observable>, ]).pipe( map(([multiContributions, activeEditor, settings, context]) => { // Merge in extra context. @@ -431,7 +431,7 @@ export function callProviders(null), + defaultIfEmpty(null), catchError(error => { logError(error) return [null] @@ -443,7 +443,7 @@ export function callProviders([]), + defaultIfEmpty([]), map(results => ({ isLoading: results.some(hover => hover === LOADING), result: mergeResult(results), diff --git a/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts b/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts index fe722decf79..c63f3e79805 100644 --- a/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts +++ b/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts @@ -61,6 +61,7 @@ describe('getDocumentHighlights from ExtensionHost API, it aims to have more e2e position: { line: 1, character: 2 }, textDocument: { uri: typescriptFileUri }, }) + // @ts-expect-error - Unclear how to consolidate the different versions of the DocumentHighlight types .subscribe(observe(value => results.push(value))) // first provider results diff --git a/client/shared/src/api/integration-test/codeEditor.test.ts b/client/shared/src/api/integration-test/codeEditor.test.ts index 98837f570a2..19a16515ec4 100644 --- a/client/shared/src/api/integration-test/codeEditor.test.ts +++ b/client/shared/src/api/integration-test/codeEditor.test.ts @@ -1,7 +1,8 @@ -import { from } from 'rxjs' +import { lastValueFrom } from 'rxjs' import { distinctUntilChanged, switchMap, take, toArray } from 'rxjs/operators' import { describe, test } from 'vitest' +import { fromSubscribable } from '@sourcegraph/common' import { Selection } from '@sourcegraph/extension-api-classes' import { assertToJSON, integrationTestContext } from '../../testing/testHelpers' @@ -11,14 +12,16 @@ describe('CodeEditor (integration)', () => { test('observe changes', async () => { const { extensionAPI, extensionHostAPI } = await integrationTestContext() - const values = from(extensionAPI.app.activeWindow!.activeViewComponentChanges) - .pipe( - switchMap(viewer => (viewer && viewer.type === 'CodeEditor' ? viewer.selectionsChanges : [])), + const values = lastValueFrom( + fromSubscribable(extensionAPI.app.activeWindow!.activeViewComponentChanges).pipe( + switchMap(viewer => + viewer && viewer.type === 'CodeEditor' ? fromSubscribable(viewer.selectionsChanges) : [] + ), distinctUntilChanged(), take(3), toArray() ) - .toPromise() + ) await extensionHostAPI.setEditorSelections({ viewerId: 'viewer#0' }, [new Selection(1, 2, 3, 4)]) await extensionHostAPI.setEditorSelections({ viewerId: 'viewer#0' }, []) diff --git a/client/shared/src/api/integration-test/documents.test.ts b/client/shared/src/api/integration-test/documents.test.ts index 5d5861a938e..c872cc4dbde 100644 --- a/client/shared/src/api/integration-test/documents.test.ts +++ b/client/shared/src/api/integration-test/documents.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'vitest' +import { fromSubscribable } from '@sourcegraph/common' + import type { TextDocument } from '../../codeintel/legacy-extensions/api' import { assertToJSON, collectSubscribableValues, integrationTestContext } from '../../testing/testHelpers' @@ -28,7 +30,7 @@ describe('Documents (integration)', () => { test('fires when a text document is opened', async () => { const { extensionAPI, extensionHostAPI } = await integrationTestContext() - const values = collectSubscribableValues(extensionAPI.workspace.openedTextDocuments) + const values = collectSubscribableValues(fromSubscribable(extensionAPI.workspace.openedTextDocuments)) expect(values).toEqual([] as TextDocument[]) await extensionHostAPI.addTextDocumentIfNotExists({ uri: 'file:///f2', languageId: 'l2', text: 't2' }) diff --git a/client/shared/src/api/integration-test/roots.test.ts b/client/shared/src/api/integration-test/roots.test.ts index b27259a4b96..f968014d375 100644 --- a/client/shared/src/api/integration-test/roots.test.ts +++ b/client/shared/src/api/integration-test/roots.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'vitest' +import { fromSubscribable } from '@sourcegraph/common' + import type { WorkspaceRoot } from '../../codeintel/legacy-extensions/api' import { collectSubscribableValues, integrationTestContext } from '../../testing/testHelpers' @@ -34,7 +36,7 @@ describe('Workspace roots (integration)', () => { test('fires when a root is added or removed', async () => { const { extensionAPI, extensionHostAPI } = await integrationTestContext() - const values = collectSubscribableValues(extensionAPI.workspace.rootChanges) + const values = collectSubscribableValues(fromSubscribable(extensionAPI.workspace.rootChanges)) expect(values).toEqual([] as void[]) await extensionHostAPI.addWorkspaceRoot({ diff --git a/client/shared/src/api/integration-test/selections.test.ts b/client/shared/src/api/integration-test/selections.test.ts index 42f99142fe1..33d23ac0c44 100644 --- a/client/shared/src/api/integration-test/selections.test.ts +++ b/client/shared/src/api/integration-test/selections.test.ts @@ -1,8 +1,7 @@ -import { from } from 'rxjs' import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators' import { describe, test } from 'vitest' -import { isDefined, isTaggedUnionMember } from '@sourcegraph/common' +import { fromSubscribable, isDefined, isTaggedUnionMember } from '@sourcegraph/common' import { assertToJSON, collectSubscribableValues, integrationTestContext } from '../../testing/testHelpers' @@ -10,13 +9,13 @@ describe('Selections (integration)', () => { describe('editor.selectionsChanged', () => { test('reflects changes to the current selections', async () => { const { extensionAPI, extensionHostAPI } = await integrationTestContext() - const selectionChanges = from(extensionAPI.app.activeWindowChanges).pipe( + const selectionChanges = fromSubscribable(extensionAPI.app.activeWindowChanges).pipe( filter(isDefined), - switchMap(window => window.activeViewComponentChanges), + switchMap(window => fromSubscribable(window.activeViewComponentChanges)), filter(isDefined), filter(isTaggedUnionMember('type', 'CodeEditor' as const)), distinctUntilChanged(), - switchMap(editor => editor.selectionsChanges) + switchMap(editor => fromSubscribable(editor.selectionsChanges)) ) const selectionValues = collectSubscribableValues(selectionChanges) const testValues = [ diff --git a/client/shared/src/api/integration-test/windows.test.ts b/client/shared/src/api/integration-test/windows.test.ts index d3d35e117c5..10d702aadae 100644 --- a/client/shared/src/api/integration-test/windows.test.ts +++ b/client/shared/src/api/integration-test/windows.test.ts @@ -1,9 +1,11 @@ import { pick } from 'lodash' -import { from, of } from 'rxjs' +import { lastValueFrom, of } from 'rxjs' import { switchMap, take, toArray } from 'rxjs/operators' import type { ViewComponent, Window } from 'sourcegraph' import { describe, expect, test } from 'vitest' +import { fromSubscribable } from '@sourcegraph/common' + import { assertToJSON, integrationTestContext } from '../../testing/testHelpers' import type { TextDocumentData } from '../viewerTypes' @@ -159,13 +161,15 @@ describe('Windows (integration)', () => { viewers: [], }) - const viewers = from(extensionAPI.app.activeWindowChanges) - .pipe( - switchMap(activeWindow => (activeWindow ? activeWindow.activeViewComponentChanges : of(null))), + const viewers = lastValueFrom( + fromSubscribable(extensionAPI.app.activeWindowChanges).pipe( + switchMap(activeWindow => + activeWindow ? fromSubscribable(activeWindow.activeViewComponentChanges) : of(null) + ), take(4), toArray() ) - .toPromise() + ) await extensionHostAPI.addTextDocumentIfNotExists({ uri: 'foo', languageId: 'l1', text: 't1' }) await extensionHostAPI.addTextDocumentIfNotExists({ uri: 'bar', languageId: 'l2', text: 't2' }) diff --git a/client/shared/src/api/util.ts b/client/shared/src/api/util.ts index 8689fbf3c15..8d06e0ce5e9 100644 --- a/client/shared/src/api/util.ts +++ b/client/shared/src/api/util.ts @@ -6,14 +6,8 @@ import { type Remote, proxyMarker, } from 'comlink' -import { - type Unsubscribable, - type Subscribable, - Observable, - type Observer, - type PartialObserver, - Subscription, -} from 'rxjs' +import { type Unsubscribable, Observable, type Observer, type PartialObserver, Subscription } from 'rxjs' +import { Subscribable } from 'sourcegraph' import { hasProperty, AbortError } from '@sourcegraph/common' diff --git a/client/shared/src/codeintel/api.ts b/client/shared/src/codeintel/api.ts index 1d67dbe9ae2..ac80f7bd757 100644 --- a/client/shared/src/codeintel/api.ts +++ b/client/shared/src/codeintel/api.ts @@ -1,5 +1,5 @@ import { castArray } from 'lodash' -import { from, type Observable, of } from 'rxjs' +import { from, of, lastValueFrom } from 'rxjs' import { defaultIfEmpty, map } from 'rxjs/operators' import { @@ -42,7 +42,7 @@ export interface CodeIntelAPI { scipParameters?: ScipParameters ): Promise getImplementations(parameters: TextDocumentPositionParameters): Promise - getHover(textParameters: TextDocumentPositionParameters): Promise + getHover(textParameters: TextDocumentPositionParameters): Promise getDocumentHighlights(textParameters: TextDocumentPositionParameters): Promise } @@ -57,9 +57,9 @@ export async function getOrCreateCodeIntelAPI(context: PlatformContext): Promise return codeIntelAPI } - return new Promise((resolve, reject) => { - context.settings.subscribe(settingsCascade => { - try { + return lastValueFrom( + context.settings.pipe( + map(settingsCascade => { if (!isSettingsValid(settingsCascade)) { throw new Error('Settings are not valid') } @@ -68,28 +68,27 @@ export async function getOrCreateCodeIntelAPI(context: PlatformContext): Promise telemetryService: context.telemetryService, settings: newSettingsGetter(settingsCascade), }) - resolve(codeIntelAPI) - } catch (error) { - reject(error) - } - }) - }) + return codeIntelAPI + }) + ) + ) } class DefaultCodeIntelAPI implements CodeIntelAPI { private locationResult( locations: sourcegraph.ProviderResult ): Promise { - return locations - .pipe( - defaultIfEmpty(), + return lastValueFrom( + locations.pipe( + defaultIfEmpty(undefined), map(result => castArray(result) .filter(isDefined) .map(location => ({ ...location, uri: location.uri.toString() })) ) - ) - .toPromise() + ), + { defaultValue: [] } + ) } public hasReferenceProvidersForDocument(textParameters: TextDocumentPositionParameters): Promise { @@ -128,26 +127,26 @@ class DefaultCodeIntelAPI implements CodeIntelAPI { request.providers.implementations.provideLocations(request.document, request.position) ) } - public getHover(textParameters: TextDocumentPositionParameters): Promise { + public getHover(textParameters: TextDocumentPositionParameters): Promise { const request = requestFor(textParameters) - return ( + return lastValueFrom( request.providers.hover .provideHover(request.document, request.position) // We intentionally don't use `defaultIfEmpty()` here because // that makes the popover load with an empty docstring. - .pipe(map(result => fromHoverMerged([result]))) - .toPromise() + .pipe(map(result => fromHoverMerged([result]))), + { defaultValue: null } ) } public getDocumentHighlights(textParameters: TextDocumentPositionParameters): Promise { const request = requestFor(textParameters) - return request.providers.documentHighlights - .provideDocumentHighlights(request.document, request.position) - .pipe( - defaultIfEmpty(), + return lastValueFrom( + request.providers.documentHighlights.provideDocumentHighlights(request.document, request.position).pipe( + defaultIfEmpty(undefined), map(result => result || []) - ) - .toPromise() + ), + { defaultValue: [] } + ) } } @@ -242,13 +241,8 @@ export function injectNewCodeintel( } export function newCodeIntelExtensionHostAPI(codeintel: CodeIntelAPI): CodeIntelExtensionHostAPI { - function thenMaybeLoadingResult(promise: Observable): Observable> { - return promise.pipe( - map(result => { - const maybeLoadingResult: MaybeLoadingResult = { isLoading: false, result } - return maybeLoadingResult - }) - ) + function thenMaybeLoadingResult(result: T): MaybeLoadingResult { + return { isLoading: false, result } } return { @@ -257,22 +251,22 @@ export function newCodeIntelExtensionHostAPI(codeintel: CodeIntelAPI): CodeIntel }, getLocations(id, parameters) { if (!id.startsWith('implementations_')) { - return proxySubscribable(thenMaybeLoadingResult(of([]))) + return proxySubscribable(of({ isLoading: false, result: [] })) } - return proxySubscribable(thenMaybeLoadingResult(from(codeintel.getImplementations(parameters)))) + return proxySubscribable(from(codeintel.getImplementations(parameters).then(thenMaybeLoadingResult))) }, getDefinition(parameters) { - return proxySubscribable(thenMaybeLoadingResult(from(codeintel.getDefinition(parameters)))) + return proxySubscribable(from(codeintel.getDefinition(parameters).then(thenMaybeLoadingResult))) }, getReferences(parameters, context, scipParameters) { return proxySubscribable( - thenMaybeLoadingResult(from(codeintel.getReferences(parameters, context, scipParameters))) + from(codeintel.getReferences(parameters, context, scipParameters).then(thenMaybeLoadingResult)) ) }, getDocumentHighlights: (textParameters: TextDocumentPositionParameters) => proxySubscribable(from(codeintel.getDocumentHighlights(textParameters))), getHover: (textParameters: TextDocumentPositionParameters) => - proxySubscribable(thenMaybeLoadingResult(from(codeintel.getHover(textParameters)))), + proxySubscribable(from(codeintel.getHover(textParameters).then(thenMaybeLoadingResult))), } } diff --git a/client/shared/src/codeintel/legacy-extensions/api.ts b/client/shared/src/codeintel/legacy-extensions/api.ts index 32b98d6ee6b..04ea5b2266c 100644 --- a/client/shared/src/codeintel/legacy-extensions/api.ts +++ b/client/shared/src/codeintel/legacy-extensions/api.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Observable, Unsubscribable } from 'rxjs' +import { lastValueFrom, type Observable, type Unsubscribable } from 'rxjs' import type { GraphQLResult } from '@sourcegraph/http-client' @@ -361,10 +361,9 @@ export function requestGraphQL(query: string, vars?: { [name: string]: unknow ) ) } - return context - - .requestGraphQL({ request: query, variables: vars as any, mightContainPrivateInfo: true }) - .toPromise() + return lastValueFrom( + context.requestGraphQL({ request: query, variables: vars as any, mightContainPrivateInfo: true }) + ) } export function getSetting(key: string): T | undefined { diff --git a/client/shared/src/hover/actions.test.ts b/client/shared/src/hover/actions.test.ts index 35e88eab42b..74973a39701 100644 --- a/client/shared/src/hover/actions.test.ts +++ b/client/shared/src/hover/actions.test.ts @@ -396,7 +396,7 @@ describe('getDefinitionURL', () => { Partial ) => '' ) - await of>({ + await of({ isLoading: false, result: [{ uri: 'git://r3?c3#f' }], }) @@ -424,7 +424,7 @@ describe('getDefinitionURL', () => { describe('when the result is inside the current root', () => { it('emits the definition URL the user input revision (not commit SHA) of the root', () => expect( - of>({ + of({ isLoading: false, result: [{ uri: 'git://r3?c3#f' }], }) @@ -445,7 +445,7 @@ describe('getDefinitionURL', () => { describe('when the result is not inside the current root (different repo and/or commit)', () => { it('emits the definition URL with range', () => expect( - of>({ + of({ isLoading: false, result: [FIXTURE_LOCATION_CLIENT], }) @@ -464,7 +464,7 @@ describe('getDefinitionURL', () => { it('emits the definition URL without range', () => expect( - of>({ + of({ isLoading: false, result: [{ ...FIXTURE_LOCATION_CLIENT, range: undefined }], }) @@ -485,7 +485,7 @@ describe('getDefinitionURL', () => { it('emits the definition panel URL if there is more than 1 location result', () => expect( - of>({ + of({ isLoading: false, result: [FIXTURE_LOCATION_CLIENT, { ...FIXTURE_LOCATION, uri: 'other' }], }) diff --git a/client/shared/src/hover/actions.ts b/client/shared/src/hover/actions.ts index 0ac00e3f780..04f00cac531 100644 --- a/client/shared/src/hover/actions.ts +++ b/client/shared/src/hover/actions.ts @@ -1,7 +1,18 @@ import type { Remote } from 'comlink' import * as H from 'history' import { isEqual, uniqWith } from 'lodash' -import { combineLatest, merge, type Observable, of, Subscription, type Unsubscribable, concat, from, EMPTY } from 'rxjs' +import { + combineLatest, + merge, + type Observable, + of, + Subscription, + type Unsubscribable, + concat, + from, + EMPTY, + lastValueFrom, +} from 'rxjs' import { catchError, delay, @@ -246,7 +257,7 @@ export const getDefinitionURL = Partial> > => { if (definitions.length === 0) { - return of>({ isLoading, result: null }) + return of({ isLoading, result: null }) } // Get unique definitions. @@ -258,7 +269,7 @@ export const getDefinitionURL = workspaceRoots || [], parseRepoURI(parameters.textDocument.uri) ) - return of>({ + return of({ isLoading, result: { url: urlToFile( @@ -409,8 +420,8 @@ export function registerHoverContributions({ const parameters: TextDocumentPositionParameters & URLToFileContext = JSON.parse(parametersString) - const { result } = await wrapRemoteObservable(extensionHostAPI.getDefinition(parameters)) - .pipe( + const { result } = await lastValueFrom( + wrapRemoteObservable(extensionHostAPI.getDefinition(parameters)).pipe( getDefinitionURL( { urlToFile, requestGraphQL }, { @@ -425,7 +436,7 @@ export function registerHoverContributions({ ), first(({ isLoading, result }) => !isLoading || result !== null) ) - .toPromise() + ) if (!result) { throw new Error('No definition found.') diff --git a/client/shared/src/platform/context.ts b/client/shared/src/platform/context.ts index 4d88ec3f2f3..2cd929ebb54 100644 --- a/client/shared/src/platform/context.ts +++ b/client/shared/src/platform/context.ts @@ -1,6 +1,6 @@ import type { Endpoint } from 'comlink' import { isObject } from 'lodash' -import type { Observable, Subscribable, Subscription } from 'rxjs' +import type { Observable, Subscription } from 'rxjs' import type { DiffPart } from '@sourcegraph/codeintellify' import { hasProperty } from '@sourcegraph/common' @@ -73,7 +73,7 @@ export interface PlatformContext { * * @deprecated Use useSettings instead */ - readonly settings: Subscribable> + readonly settings: Observable> /** * Update the settings for the subject, either by inserting/changing a specific value or by overwriting the diff --git a/client/shared/src/testing/integration/context.ts b/client/shared/src/testing/integration/context.ts index a4921064047..ff8e46a74a5 100644 --- a/client/shared/src/testing/integration/context.ts +++ b/client/shared/src/testing/integration/context.ts @@ -10,7 +10,7 @@ import type { Test } from 'mocha' import { readFile, mkdir } from 'mz/fs' import pTimeout from 'p-timeout' import * as prettier from 'prettier' -import { Subject, Subscription, throwError } from 'rxjs' +import { Subject, Subscription, lastValueFrom, throwError } from 'rxjs' import { first, timeoutWith } from 'rxjs/operators' import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config' @@ -288,15 +288,15 @@ export const createSharedIntegrationTestContext = async < triggerRequest: () => Promise | void, operationName: O ): Promise[0]> => { - const requestPromise = graphQlRequests - .pipe( + const requestPromise = lastValueFrom( + graphQlRequests.pipe( first( (request: GraphQLRequestEvent): request is GraphQLRequestEvent => request.operationName === operationName ), timeoutWith(4000, throwError(new Error(`Timeout waiting for GraphQL request "${operationName}"`))) ) - .toPromise() + ) await triggerRequest() const { variables } = await requestPromise return variables diff --git a/client/shared/src/testing/testHelpers.ts b/client/shared/src/testing/testHelpers.ts index efe5424c8d6..ec105ad1b15 100644 --- a/client/shared/src/testing/testHelpers.ts +++ b/client/shared/src/testing/testHelpers.ts @@ -1,5 +1,5 @@ import type { Remote } from 'comlink' -import { throwError, of, Subscription, type Unsubscribable, type Subscribable } from 'rxjs' +import { throwError, of, Subscription, type Unsubscribable, type Observable } from 'rxjs' import type * as sourcegraph from 'sourcegraph' import { expect } from 'vitest' @@ -107,8 +107,9 @@ export async function integrationTestContext( } } -export function collectSubscribableValues(subscribable: Subscribable): T[] { +export function collectSubscribableValues(observable: Observable): T[] { const values: T[] = [] - subscribable.subscribe(value => values.push(value)) + // eslint-disable-next-line rxjs/no-ignored-subscription + observable.subscribe(value => values.push(value)) return values } diff --git a/client/shared/src/util/useInputValidation.ts b/client/shared/src/util/useInputValidation.ts index 00445169762..46cdb3e27c7 100644 --- a/client/shared/src/util/useInputValidation.ts +++ b/client/shared/src/util/useInputValidation.ts @@ -180,7 +180,7 @@ export function createValidationPipeline( combineLatest([ // Validate immediately if the user has provided an initial input value concat( - initialValue !== undefined ? of({ value: initialValue, validate: true }) : EMPTY, + initialValue !== undefined ? of({ value: initialValue, validate: true }) : EMPTY, inputValidationEvents ), inputReferences, diff --git a/client/web-sveltekit/src/lib/graphql/urql.ts b/client/web-sveltekit/src/lib/graphql/urql.ts index 78842e6d5bb..5c29cf85ee7 100644 --- a/client/web-sveltekit/src/lib/graphql/urql.ts +++ b/client/web-sveltekit/src/lib/graphql/urql.ts @@ -206,7 +206,7 @@ export function infinityQuery>>( + return concat( of({ fetching: true, stale: false, restoring: false }), from(args.client.executeRequestOperation(operation).toPromise()).pipe( map(({ data, stale, operation, error, extensions }) => ({ diff --git a/client/web-sveltekit/vite.config.ts b/client/web-sveltekit/vite.config.ts index 32c657b941a..895a668fe29 100644 --- a/client/web-sveltekit/vite.config.ts +++ b/client/web-sveltekit/vite.config.ts @@ -73,33 +73,6 @@ export default defineConfig(({ mode }) => { find: /^(.*)\.gql$/, replacement: '$1.gql.ts', }, - // In rxjs v6 these are directories and cannot be imported from directly in the production build. - // The following error occurs: - // Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '[...]/node_modules/rxjs/operators' is not supported resolving ES modules - { - find: /^rxjs\/(operators|fetch)$/, - replacement: 'rxjs/$1/index.js', - customResolver(source, importer, options) { - // This is an hacky way to make the dev build work. @sourcegraph/telemetry uses a newer - // version of rxjs (v7) where `rjx/operators` and `rxjs/fetch` are properly mapped - // to their respective files in package.json. - // Applying the same replacement to this version results in an error. - // I tried various ways to prevent having the alias be applied to `@sourcegraph/telemetry` - // without success: - // - Removing this alias causes the production build to fail do the issue mentioned at the - // top of this alias. - // - Adding something like `{ ssr: { external: '@sourcegraph/telemetry' } }` to the config - // does not prevent the alias from being applied. Maybe I don't understand how `external` - // is supposed to work. - // - Using a custom plugin that implements a custom resolveId function is somehow not being - // run in the production build for `rxjs` imports. Maybe it has something to do with the - // interop between vite and sveltekit. - if (importer?.includes('@sourcegraph/telemetry')) { - source = source.replace('/index.js', '') - } - return this.resolve(source, importer, options) - }, - }, // Without aliasing lodash to lodash-es we get the following error: // SyntaxError: Named export 'castArray' not found. The requested module 'lodash' is a CommonJS module, which may not support all module.exports as named exports. { diff --git a/client/web/dev/BUILD.bazel b/client/web/dev/BUILD.bazel index e4717929267..6629934ea84 100644 --- a/client/web/dev/BUILD.bazel +++ b/client/web/dev/BUILD.bazel @@ -89,10 +89,6 @@ esbuild( "path-browserify", "monaco-yaml/lib/esm/monaco.contribution", "monaco-yaml/lib/esm/yaml.worker", - "rxjs/_esm5/internal/OuterSubscriber", - "rxjs/_esm5/internal/util/subscribeToResult", - "rxjs/_esm5/internal/util/subscribeToArray", - "rxjs/_esm5/internal/Observable", ], format = "cjs", platform = "node", diff --git a/client/web/dev/esbuild/config.ts b/client/web/dev/esbuild/config.ts index 596dc586911..ffe0e90d368 100644 --- a/client/web/dev/esbuild/config.ts +++ b/client/web/dev/esbuild/config.ts @@ -8,7 +8,6 @@ import { stylePlugin, packageResolutionPlugin, monacoPlugin, - RXJS_RESOLUTIONS, buildTimerPlugin, workerPlugin, } from '@sourcegraph/build-config/src/esbuild/plugins' @@ -51,7 +50,6 @@ export function esbuildBuildOptions(ENVIRONMENT_CONFIG: EnvironmentConfig): esbu workerPlugin, packageResolutionPlugin({ path: require.resolve('path-browserify'), - ...RXJS_RESOLUTIONS, ...(ENVIRONMENT_CONFIG.DEV_WEB_BUILDER_OMIT_SLOW_DEPS ? { // Monaco diff --git a/client/web/src/SearchQueryStateObserver.tsx b/client/web/src/SearchQueryStateObserver.tsx index d6710ddb0a1..4610a1a3d30 100644 --- a/client/web/src/SearchQueryStateObserver.tsx +++ b/client/web/src/SearchQueryStateObserver.tsx @@ -1,8 +1,7 @@ import { type FC, useLayoutEffect, useRef, useState } from 'react' import { type Location, useLocation } from 'react-router-dom' -import { BehaviorSubject } from 'rxjs' -import { first } from 'rxjs/operators' +import { BehaviorSubject, firstValueFrom } from 'rxjs' import type { PlatformContext } from '@sourcegraph/shared/src/platform/context' import { isSearchContextSpecAvailable } from '@sourcegraph/shared/src/search' @@ -45,12 +44,13 @@ export const SearchQueryStateObserver: FC = props location: locationSubject, isSearchContextAvailable: (searchContext: string) => searchContextsEnabled - ? isSearchContextSpecAvailable({ - spec: searchContext, - platformContext, - }) - .pipe(first()) - .toPromise() + ? firstValueFrom( + isSearchContextSpecAvailable({ + spec: searchContext, + platformContext, + }), + { defaultValue: false } + ) : Promise.resolve(false), }).subscribe(parsedSearchURLAndContext => { if (parsedSearchURLAndContext.query) { diff --git a/client/web/src/components/externalServices/backend.ts b/client/web/src/components/externalServices/backend.ts index 5ab2a4fbd0d..b46038b96fa 100644 --- a/client/web/src/components/externalServices/backend.ts +++ b/client/web/src/components/externalServices/backend.ts @@ -2,7 +2,7 @@ import type { Dispatch, SetStateAction } from 'react' import type { QueryTuple, MutationTuple, QueryResult } from '@apollo/client' import { parse } from 'jsonc-parser' -import type { Observable } from 'rxjs' +import { type Observable, lastValueFrom } from 'rxjs' import { map } from 'rxjs/operators' import { createAggregateError } from '@sourcegraph/common' @@ -111,28 +111,30 @@ export const useUpdateExternalService = ( export function updateExternalService( variables: UpdateExternalServiceVariables ): Promise { - return requestGraphQL( - UPDATE_EXTERNAL_SERVICE, - variables - ) - .pipe( + return lastValueFrom( + requestGraphQL( + UPDATE_EXTERNAL_SERVICE, + variables + ).pipe( map(dataOrThrowErrors), map(data => data.updateExternalService) ) - .toPromise() + ) } export async function deleteExternalService(externalService: Scalars['ID']): Promise { - const result = await requestGraphQL( - gql` - mutation DeleteExternalService($externalService: ID!) { - deleteExternalService(externalService: $externalService) { - alwaysNil + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation DeleteExternalService($externalService: ID!) { + deleteExternalService(externalService: $externalService) { + alwaysNil + } } - } - `, - { externalService } - ).toPromise() + `, + { externalService } + ) + ) dataOrThrowErrors(result) } diff --git a/client/web/src/enterprise/batches/close/backend.ts b/client/web/src/enterprise/batches/close/backend.ts index 5730b884c65..bfbb716595e 100644 --- a/client/web/src/enterprise/batches/close/backend.ts +++ b/client/web/src/enterprise/batches/close/backend.ts @@ -1,18 +1,22 @@ +import { lastValueFrom } from 'rxjs' + import { gql, dataOrThrowErrors } from '@sourcegraph/http-client' import { requestGraphQL } from '../../../backend/graphql' import type { CloseBatchChangeResult, CloseBatchChangeVariables } from '../../../graphql-operations' export async function closeBatchChange({ batchChange, closeChangesets }: CloseBatchChangeVariables): Promise { - const result = await requestGraphQL( - gql` - mutation CloseBatchChange($batchChange: ID!, $closeChangesets: Boolean) { - closeBatchChange(batchChange: $batchChange, closeChangesets: $closeChangesets) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation CloseBatchChange($batchChange: ID!, $closeChangesets: Boolean) { + closeBatchChange(batchChange: $batchChange, closeChangesets: $closeChangesets) { + id + } } - } - `, - { batchChange, closeChangesets } - ).toPromise() + `, + { batchChange, closeChangesets } + ) + ) dataOrThrowErrors(result) } diff --git a/client/web/src/enterprise/batches/detail/backend.ts b/client/web/src/enterprise/batches/detail/backend.ts index 3229d8ad5d0..3ae3ae73beb 100644 --- a/client/web/src/enterprise/batches/detail/backend.ts +++ b/client/web/src/enterprise/batches/detail/backend.ts @@ -1,5 +1,5 @@ import type { QueryResult, QueryTuple } from '@apollo/client' -import { EMPTY, type Observable } from 'rxjs' +import { EMPTY, lastValueFrom, type Observable } from 'rxjs' import { expand, map, reduce } from 'rxjs/operators' import { dataOrThrowErrors, gql, useLazyQuery, useQuery } from '@sourcegraph/http-client' @@ -459,37 +459,39 @@ export const queryChangesets = ({ ) export async function syncChangeset(changeset: Scalars['ID']): Promise { - const result = await requestGraphQL( - gql` - mutation SyncChangeset($changeset: ID!) { - syncChangeset(changeset: $changeset) { - alwaysNil + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation SyncChangeset($changeset: ID!) { + syncChangeset(changeset: $changeset) { + alwaysNil + } } - } - `, - { changeset } - ).toPromise() + `, + { changeset } + ) + ) dataOrThrowErrors(result) } export async function reenqueueChangeset(changeset: Scalars['ID']): Promise { - return requestGraphQL( - gql` - mutation ReenqueueChangeset($changeset: ID!) { - reenqueueChangeset(changeset: $changeset) { - ...ChangesetFields + return lastValueFrom( + requestGraphQL( + gql` + mutation ReenqueueChangeset($changeset: ID!) { + reenqueueChangeset(changeset: $changeset) { + ...ChangesetFields + } } - } - ${changesetFieldsFragment} - `, - { changeset } - ) - .pipe( + ${changesetFieldsFragment} + `, + { changeset } + ).pipe( map(dataOrThrowErrors), map(data => data.reenqueueChangeset) ) - .toPromise() + ) } // Because thats the name in the API: @@ -628,16 +630,18 @@ export const useChangesetCountsOverTime = ( }) export async function deleteBatchChange(batchChange: Scalars['ID']): Promise { - const result = await requestGraphQL( - gql` - mutation DeleteBatchChange($batchChange: ID!) { - deleteBatchChange(batchChange: $batchChange) { - alwaysNil + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation DeleteBatchChange($batchChange: ID!) { + deleteBatchChange(batchChange: $batchChange) { + alwaysNil + } } - } - `, - { batchChange } - ).toPromise() + `, + { batchChange } + ) + ) dataOrThrowErrors(result) } @@ -657,20 +661,20 @@ const changesetDiffFragment = gql` ` export async function getChangesetDiff(changeset: Scalars['ID']): Promise { - return requestGraphQL( - gql` - query ChangesetDiff($changeset: ID!) { - node(id: $changeset) { - __typename - ...ChangesetDiffFields + return lastValueFrom( + requestGraphQL( + gql` + query ChangesetDiff($changeset: ID!) { + node(id: $changeset) { + __typename + ...ChangesetDiffFields + } } - } - ${changesetDiffFragment} - `, - { changeset } - ) - .pipe( + ${changesetDiffFragment} + `, + { changeset } + ).pipe( map(dataOrThrowErrors), map(({ node }) => { if (!node) { @@ -693,7 +697,7 @@ export async function getChangesetDiff(changeset: Scalars['ID']): Promise { - return requestGraphQL( - gql` - query ChangesetScheduleEstimate($changeset: ID!) { - node(id: $changeset) { - __typename - ...ChangesetScheduleEstimateFields + return lastValueFrom( + requestGraphQL( + gql` + query ChangesetScheduleEstimate($changeset: ID!) { + node(id: $changeset) { + __typename + ...ChangesetScheduleEstimateFields + } } - } - ${changesetScheduleEstimateFragment} - `, - { changeset } - ) - .pipe( + ${changesetScheduleEstimateFragment} + `, + { changeset } + ).pipe( map(dataOrThrowErrors), map(({ node }) => { if (!node) { @@ -730,20 +734,22 @@ export async function getChangesetScheduleEstimate(changeset: Scalars['ID']): Pr return node.scheduleEstimateAt }) ) - .toPromise() + ) } export async function detachChangesets(batchChange: Scalars['ID'], changesets: Scalars['ID'][]): Promise { - const result = await requestGraphQL( - gql` - mutation DetachChangesets($batchChange: ID!, $changesets: [ID!]!) { - detachChangesets(batchChange: $batchChange, changesets: $changesets) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation DetachChangesets($batchChange: ID!, $changesets: [ID!]!) { + detachChangesets(batchChange: $batchChange, changesets: $changesets) { + id + } } - } - `, - { batchChange, changesets } - ).toPromise() + `, + { batchChange, changesets } + ) + ) dataOrThrowErrors(result) } @@ -752,30 +758,34 @@ export async function createChangesetComments( changesets: Scalars['ID'][], body: string ): Promise { - const result = await requestGraphQL( - gql` - mutation CreateChangesetComments($batchChange: ID!, $changesets: [ID!]!, $body: String!) { - createChangesetComments(batchChange: $batchChange, changesets: $changesets, body: $body) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation CreateChangesetComments($batchChange: ID!, $changesets: [ID!]!, $body: String!) { + createChangesetComments(batchChange: $batchChange, changesets: $changesets, body: $body) { + id + } } - } - `, - { batchChange, changesets, body } - ).toPromise() + `, + { batchChange, changesets, body } + ) + ) dataOrThrowErrors(result) } export async function reenqueueChangesets(batchChange: Scalars['ID'], changesets: Scalars['ID'][]): Promise { - const result = await requestGraphQL( - gql` - mutation ReenqueueChangesets($batchChange: ID!, $changesets: [ID!]!) { - reenqueueChangesets(batchChange: $batchChange, changesets: $changesets) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation ReenqueueChangesets($batchChange: ID!, $changesets: [ID!]!) { + reenqueueChangesets(batchChange: $batchChange, changesets: $changesets) { + id + } } - } - `, - { batchChange, changesets } - ).toPromise() + `, + { batchChange, changesets } + ) + ) dataOrThrowErrors(result) } @@ -784,30 +794,34 @@ export async function mergeChangesets( changesets: Scalars['ID'][], squash: boolean ): Promise { - const result = await requestGraphQL( - gql` - mutation MergeChangesets($batchChange: ID!, $changesets: [ID!]!, $squash: Boolean!) { - mergeChangesets(batchChange: $batchChange, changesets: $changesets, squash: $squash) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation MergeChangesets($batchChange: ID!, $changesets: [ID!]!, $squash: Boolean!) { + mergeChangesets(batchChange: $batchChange, changesets: $changesets, squash: $squash) { + id + } } - } - `, - { batchChange, changesets, squash } - ).toPromise() + `, + { batchChange, changesets, squash } + ) + ) dataOrThrowErrors(result) } export async function closeChangesets(batchChange: Scalars['ID'], changesets: Scalars['ID'][]): Promise { - const result = await requestGraphQL( - gql` - mutation CloseChangesets($batchChange: ID!, $changesets: [ID!]!) { - closeChangesets(batchChange: $batchChange, changesets: $changesets) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation CloseChangesets($batchChange: ID!, $changesets: [ID!]!) { + closeChangesets(batchChange: $batchChange, changesets: $changesets) { + id + } } - } - `, - { batchChange, changesets } - ).toPromise() + `, + { batchChange, changesets } + ) + ) dataOrThrowErrors(result) } @@ -816,16 +830,18 @@ export async function publishChangesets( changesets: Scalars['ID'][], draft: boolean ): Promise { - const result = await requestGraphQL( - gql` - mutation PublishChangesets($batchChange: ID!, $changesets: [ID!]!, $draft: Boolean!) { - publishChangesets(batchChange: $batchChange, changesets: $changesets, draft: $draft) { - id + const result = await lastValueFrom( + requestGraphQL( + gql` + mutation PublishChangesets($batchChange: ID!, $changesets: [ID!]!, $draft: Boolean!) { + publishChangesets(batchChange: $batchChange, changesets: $changesets, draft: $draft) { + id + } } - } - `, - { batchChange, changesets, draft } - ).toPromise() + `, + { batchChange, changesets, draft } + ) + ) dataOrThrowErrors(result) } diff --git a/client/web/src/enterprise/batches/preview/backend.ts b/client/web/src/enterprise/batches/preview/backend.ts index 1c56fea8dce..cadd6b49c04 100644 --- a/client/web/src/enterprise/batches/preview/backend.ts +++ b/client/web/src/enterprise/batches/preview/backend.ts @@ -1,4 +1,4 @@ -import type { Observable } from 'rxjs' +import { lastValueFrom, type Observable } from 'rxjs' import { map } from 'rxjs/operators' import { gql, dataOrThrowErrors } from '@sourcegraph/http-client' @@ -139,49 +139,49 @@ export const createBatchChange = ({ batchSpec, publicationStates, }: CreateBatchChangeVariables): Promise => - requestGraphQL( - gql` - mutation CreateBatchChange($batchSpec: ID!, $publicationStates: [ChangesetSpecPublicationStateInput!]) { - createBatchChange(batchSpec: $batchSpec, publicationStates: $publicationStates) { - id - url + lastValueFrom( + requestGraphQL( + gql` + mutation CreateBatchChange($batchSpec: ID!, $publicationStates: [ChangesetSpecPublicationStateInput!]) { + createBatchChange(batchSpec: $batchSpec, publicationStates: $publicationStates) { + id + url + } } - } - `, - { batchSpec, publicationStates } - ) - .pipe( + `, + { batchSpec, publicationStates } + ).pipe( map(dataOrThrowErrors), map(data => data.createBatchChange) ) - .toPromise() + ) export const applyBatchChange = ({ batchSpec, batchChange, publicationStates, }: ApplyBatchChangeVariables): Promise => - requestGraphQL( - gql` - mutation ApplyBatchChange( - $batchSpec: ID! - $batchChange: ID! - $publicationStates: [ChangesetSpecPublicationStateInput!] - ) { - applyBatchChange( - batchSpec: $batchSpec - ensureBatchChange: $batchChange - publicationStates: $publicationStates + lastValueFrom( + requestGraphQL( + gql` + mutation ApplyBatchChange( + $batchSpec: ID! + $batchChange: ID! + $publicationStates: [ChangesetSpecPublicationStateInput!] ) { - id - url + applyBatchChange( + batchSpec: $batchSpec + ensureBatchChange: $batchChange + publicationStates: $publicationStates + ) { + id + url + } } - } - `, - { batchSpec, batchChange, publicationStates } - ) - .pipe( + `, + { batchSpec, batchChange, publicationStates } + ).pipe( map(dataOrThrowErrors), map(data => data.applyBatchChange) ) - .toPromise() + ) diff --git a/client/web/src/enterprise/codeintel/indexes/components/EnqueueForm.tsx b/client/web/src/enterprise/codeintel/indexes/components/EnqueueForm.tsx index 7362d418cec..ad4a430ab81 100644 --- a/client/web/src/enterprise/codeintel/indexes/components/EnqueueForm.tsx +++ b/client/web/src/enterprise/codeintel/indexes/components/EnqueueForm.tsx @@ -41,8 +41,8 @@ export const EnqueueForm: FunctionComponent = ({ const queueResultLength = indexes?.queueAutoIndexJobsForRepo.length || 0 setQueueResult(queueResultLength) - if (queueResultLength > 0) { - querySubject.next(indexes?.queueAutoIndexJobsForRepo[0].inputCommit) + if (queueResultLength > 0 && indexes?.queueAutoIndexJobsForRepo[0].inputCommit !== undefined) { + querySubject.next(indexes.queueAutoIndexJobsForRepo[0].inputCommit) } } catch (error) { setEnqueueError(error) diff --git a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx index 197dcba5916..eae35f829e4 100644 --- a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx +++ b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx @@ -170,7 +170,7 @@ export const CodeIntelPreciseIndexesPage: FunctionComponent new Subject(), []) + const refresh = useMemo(() => new Subject(), []) const querySubject = useMemo(() => new Subject(), []) // State used to control bulk index selection diff --git a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts index 1031ffbcdb6..708a11e474b 100644 --- a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts +++ b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts @@ -122,7 +122,7 @@ async function getLangStats(inputs: GetInsightContentInputs): Promise { throw new Error('You have to specify a dashboard visibility') } - const updatedDashboard = await updateDashboard({ - id: dashboard.id, - nextDashboardInput: { - name, - owners: [owner], - }, - }).toPromise() + const updatedDashboard = await lastValueFrom( + updateDashboard({ + id: dashboard.id, + nextDashboardInput: { + name, + owners: [owner], + }, + }) + ) navigate(`/insights/dashboards/${updatedDashboard.id}`) } diff --git a/client/web/src/enterprise/searchContexts/backend.ts b/client/web/src/enterprise/searchContexts/backend.ts index a700f202c84..1e6f9439b78 100644 --- a/client/web/src/enterprise/searchContexts/backend.ts +++ b/client/web/src/enterprise/searchContexts/backend.ts @@ -1,5 +1,6 @@ +import { lastValueFrom } from 'rxjs' + import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' -import type { GraphQLResult } from '@sourcegraph/http-client' import { requestGraphQL } from '../../backend/graphql' import type { InputMaybe, RepositoriesByNamesResult, RepositoriesByNamesVariables } from '../../graphql-operations' @@ -27,14 +28,13 @@ export async function fetchRepositoriesByNames( let after: InputMaybe = null while (true) { - const result: GraphQLResult = await requestGraphQL< - RepositoriesByNamesResult, - RepositoriesByNamesVariables - >(query, { - names, - first, - after, - }).toPromise() + const result = await lastValueFrom( + requestGraphQL(query, { + names, + first, + after, + }) + ) const data: RepositoriesByNamesResult = dataOrThrowErrors(result) diff --git a/client/web/src/org/backend.ts b/client/web/src/org/backend.ts index f9099a25a92..8cf14bb64b4 100644 --- a/client/web/src/org/backend.ts +++ b/client/web/src/org/backend.ts @@ -1,4 +1,4 @@ -import { concat, type Observable } from 'rxjs' +import { concat, lastValueFrom, type Observable } from 'rxjs' import { map, mergeMap } from 'rxjs/operators' import { createAggregateError } from '@sourcegraph/common' @@ -65,19 +65,19 @@ export function createOrganization(args: { /** The new organization's display name (e.g. full name) in the organization profile. */ displayName?: string }): Promise { - return requestGraphQL( - gql` - mutation CreateOrganization($name: String!, $displayName: String) { - createOrganization(name: $name, displayName: $displayName) { - id - name - settingsURL + return lastValueFrom( + requestGraphQL( + gql` + mutation CreateOrganization($name: String!, $displayName: String) { + createOrganization(name: $name, displayName: $displayName) { + id + name + settingsURL + } } - } - `, - { name: args.name, displayName: args.displayName ?? null } - ) - .pipe( + `, + { name: args.name, displayName: args.displayName ?? null } + ).pipe( mergeMap(({ data, errors }) => { if (!data?.createOrganization) { eventLogger.log('NewOrgFailed') @@ -87,7 +87,7 @@ export function createOrganization(args: { return concat(refreshAuthenticatedUser(), [data.createOrganization]) }) ) - .toPromise() + ) } export const REMOVE_USER_FROM_ORGANIZATION_QUERY = gql` diff --git a/client/web/src/org/settings/members/InviteForm.tsx b/client/web/src/org/settings/members/InviteForm.tsx index c4e2aa08ae2..4384442c644 100644 --- a/client/web/src/org/settings/members/InviteForm.tsx +++ b/client/web/src/org/settings/members/InviteForm.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react' import { mdiPlus, mdiEmailOpenOutline, mdiClose } from '@mdi/js' import classNames from 'classnames' +import { lastValueFrom } from 'rxjs' import { map } from 'rxjs/operators' import { asError, createAggregateError, isErrorLike } from '@sourcegraph/common' @@ -204,25 +205,25 @@ function inviteUserToOrganization( username: string, organization: Scalars['ID'] ): Promise { - return requestGraphQL( - gql` - mutation InviteUserToOrganization($organization: ID!, $username: String!) { - inviteUserToOrganization(organization: $organization, username: $username) { - ...InviteUserToOrganizationFields + return lastValueFrom( + requestGraphQL( + gql` + mutation InviteUserToOrganization($organization: ID!, $username: String!) { + inviteUserToOrganization(organization: $organization, username: $username) { + ...InviteUserToOrganizationFields + } } - } - fragment InviteUserToOrganizationFields on InviteUserToOrganizationResult { - sentInvitationEmail - invitationURL + fragment InviteUserToOrganizationFields on InviteUserToOrganizationResult { + sentInvitationEmail + invitationURL + } + `, + { + username, + organization, } - `, - { - username, - organization, - } - ) - .pipe( + ).pipe( map(({ data, errors }) => { if (!data?.inviteUserToOrganization || (errors && errors.length > 0)) { eventLogger.log('InviteOrgMemberFailed') @@ -232,7 +233,7 @@ function inviteUserToOrganization( return data.inviteUserToOrganization }) ) - .toPromise() + ) } function addUserToOrganization(username: string, organization: Scalars['ID']): Promise { diff --git a/client/web/src/regression/util/api.ts b/client/web/src/regression/util/api.ts index 781a5569501..1b71e0f4e7b 100644 --- a/client/web/src/regression/util/api.ts +++ b/client/web/src/regression/util/api.ts @@ -2,7 +2,7 @@ * Provides convenience functions for interacting with the Sourcegraph API from tests. */ -import { zip, timer, concat, throwError, defer, type Observable } from 'rxjs' +import { zip, timer, concat, throwError, defer, type Observable, lastValueFrom } from 'rxjs' import { map, tap, retryWhen, delayWhen, take, mergeMap } from 'rxjs/operators' import { isErrorLike, createAggregateError, logger } from '@sourcegraph/common' @@ -261,40 +261,41 @@ export function getExternalServices( uniqueDisplayName?: string } = {} ): Promise { - return gqlClient - .queryGraphQL( - gql` - query ExternalServicesRegression($first: Int) { - externalServices(first: $first) { - nodes { - ...ExternalServiceNodeFields + return lastValueFrom( + gqlClient + .queryGraphQL( + gql` + query ExternalServicesRegression($first: Int) { + externalServices(first: $first) { + nodes { + ...ExternalServiceNodeFields + } } } - } - fragment ExternalServiceNodeFields on ExternalService { - id - kind - displayName - config - createdAt - updatedAt - warning - } - `, - { first: 100 } - ) - .pipe( - map(dataOrThrowErrors), - map(({ externalServices }) => - externalServices.nodes.filter( - ({ displayName, kind }) => - (options.uniqueDisplayName === undefined || options.uniqueDisplayName === displayName) && - (options.kind === undefined || options.kind === kind) + fragment ExternalServiceNodeFields on ExternalService { + id + kind + displayName + config + createdAt + updatedAt + warning + } + `, + { first: 100 } + ) + .pipe( + map(dataOrThrowErrors), + map(({ externalServices }) => + externalServices.nodes.filter( + ({ displayName, kind }) => + (options.uniqueDisplayName === undefined || options.uniqueDisplayName === displayName) && + (options.kind === undefined || options.kind === kind) + ) ) ) - ) - .toPromise() + ) } export async function updateExternalService( @@ -448,22 +449,23 @@ export async function setTosAccepted(gqlClient: GraphQLClient, userID: Scalars[' * dependency-injected `requestGraphQL`. */ export function currentProductVersion(gqlClient: GraphQLClient): Promise { - return gqlClient - .queryGraphQL( - gql` - query SiteProductVersion { - site { - productVersion + return lastValueFrom( + gqlClient + .queryGraphQL( + gql` + query SiteProductVersion { + site { + productVersion + } } - } - `, - {} - ) - .pipe( - map(dataOrThrowErrors), - map(({ site }) => site.productVersion) - ) - .toPromise() + `, + {} + ) + .pipe( + map(dataOrThrowErrors), + map(({ site }) => site.productVersion) + ) + ) } /** @@ -473,16 +475,16 @@ export function currentProductVersion(gqlClient: GraphQLClient): Promise export function getViewerSettings({ requestGraphQL, }: Pick): Promise { - return requestGraphQL({ - request: viewerSettingsQuery, - variables: {}, - mightContainPrivateInfo: true, - }) - .pipe( + return lastValueFrom( + requestGraphQL({ + request: viewerSettingsQuery, + variables: {}, + mightContainPrivateInfo: true, + }).pipe( map(dataOrThrowErrors), map(data => data.viewerSettings) ) - .toPromise() + ) } /** @@ -631,55 +633,54 @@ export function createUser( * TODO(beyang): remove this after the corresponding API in the main code has been updated to use a * dependency-injected `requestGraphQL`. */ -export async function getUser( +export function getUser( { requestGraphQL }: Pick, username: string ): Promise { - const user = await requestGraphQL({ - request: gql` - query User($username: String!) { - user(username: $username) { - __typename - id - username - displayName - url - settingsURL - avatarURL - viewerCanAdminister - siteAdmin - createdAt - emails { - email - verified - } - organizations { - nodes { - id - displayName - name + return lastValueFrom( + requestGraphQL({ + request: gql` + query User($username: String!) { + user(username: $username) { + __typename + id + username + displayName + url + settingsURL + avatarURL + viewerCanAdminister + siteAdmin + createdAt + emails { + email + verified } - } - settingsCascade { - subjects { - latestSettings { + organizations { + nodes { id - contents + displayName + name + } + } + settingsCascade { + subjects { + latestSettings { + id + contents + } } } } } - } - `, - variables: { username }, - mightContainPrivateInfo: true, - }) - .pipe( + `, + variables: { username }, + mightContainPrivateInfo: true, + }).pipe( map(dataOrThrowErrors), map(({ user }) => user) ) - .toPromise() - return user + ) } /** @@ -748,86 +749,86 @@ export function search( version: SearchVersion, patternType: SearchPatternType ): Promise { - return requestGraphQL({ - request: gql` - query Search($query: String!, $version: SearchVersion!, $patternType: SearchPatternType!) { - search(query: $query, version: $version, patternType: $patternType) { - results { - __typename - limitHit - matchCount - approximateResultCount - missing { - name - } - cloning { - name - } - timedout { - name - } - indexUnavailable - dynamicFilters { - value - label - count - limitHit - kind - } + return lastValueFrom( + requestGraphQL({ + request: gql` + query Search($query: String!, $version: SearchVersion!, $patternType: SearchPatternType!) { + search(query: $query, version: $version, patternType: $patternType) { results { __typename - ... on Repository { - id + limitHit + matchCount + approximateResultCount + missing { name - ...GenericSearchResultFields } - ... on FileMatch { - file { - path - url - commit { - oid + cloning { + name + } + timedout { + name + } + indexUnavailable + dynamicFilters { + value + label + count + limitHit + kind + } + results { + __typename + ... on Repository { + id + name + ...GenericSearchResultFields + } + ... on FileMatch { + file { + path + url + commit { + oid + } + } + repository { + name + url + } + limitHit + symbols { + name + containerName + url + kind + } + lineMatches { + preview + lineNumber + offsetAndLengths } } - repository { - name - url - } - limitHit - symbols { - name - containerName - url - kind - } - lineMatches { - preview - lineNumber - offsetAndLengths + ... on CommitSearchResult { + ...GenericSearchResultFields } } - ... on CommitSearchResult { - ...GenericSearchResultFields - } - } - alert { - title - description - proposedQueries { + alert { + title description - query + proposedQueries { + description + query + } } + elapsedMilliseconds } - elapsedMilliseconds } } - } - ${GenericSearchResultInterfaceFragment} - `, - variables: { query, version, patternType }, - mightContainPrivateInfo: false, - }) - .pipe( + ${GenericSearchResultInterfaceFragment} + `, + variables: { query, version, patternType }, + mightContainPrivateInfo: false, + }).pipe( map(dataOrThrowErrors), map(data => { if (!data.search) { @@ -836,7 +837,7 @@ export function search( return data.search }) ) - .toPromise() + ) } /** diff --git a/client/web/src/regression/util/helpers.ts b/client/web/src/regression/util/helpers.ts index d14d853f2f1..a8aafe08162 100644 --- a/client/web/src/regression/util/helpers.ts +++ b/client/web/src/regression/util/helpers.ts @@ -1,6 +1,6 @@ import * as jsonc from 'jsonc-parser' import { first } from 'lodash' -import { throwError } from 'rxjs' +import { lastValueFrom, throwError } from 'rxjs' import { catchError, map } from 'rxjs/operators' import { Key } from 'ts-key-enum' @@ -131,7 +131,7 @@ export async function createAuthProvider( gqlClient: GraphQLClient, authProvider: GitHubAuthProvider | GitLabAuthProvider | OpenIDConnectAuthProvider | SAMLAuthProvider ): Promise { - const siteConfig = await fetchSiteConfiguration(gqlClient).toPromise() + const siteConfig = await lastValueFrom(fetchSiteConfiguration(gqlClient)) const siteConfigParsed: SiteConfiguration = jsonc.parse(siteConfig.configuration.effectiveContents) const authProviders = siteConfigParsed['auth.providers'] if ( @@ -185,7 +185,7 @@ export async function ensureNewOrganization( { requestGraphQL }: Pick, variables: CreateOrganizationVariables ): Promise<{ destroy: ResourceDestructor; result: CreateOrganizationResult['createOrganization'] }> { - const matchingOrgs = (await fetchAllOrganizations({ requestGraphQL }, { first: 1000 }).toPromise()).nodes.filter( + const matchingOrgs = (await lastValueFrom(fetchAllOrganizations({ requestGraphQL }, { first: 1000 }))).nodes.filter( org => org.name === variables.name ) if (matchingOrgs.length > 1) { @@ -194,7 +194,7 @@ export async function ensureNewOrganization( if (matchingOrgs.length === 1) { await deleteOrganization({ requestGraphQL }, matchingOrgs[0].id).toPromise() } - const createdOrg = await createOrganization({ requestGraphQL }, variables).toPromise() + const createdOrg = await lastValueFrom(createOrganization({ requestGraphQL }, variables)) return { destroy: () => deleteOrganization({ requestGraphQL }, createdOrg.id).toPromise(), result: createdOrg, @@ -239,15 +239,15 @@ export async function editSiteConfig( gqlClient: GraphQLClient, ...edits: ((contents: string) => jsonc.Edit[])[] ): Promise<{ destroy: ResourceDestructor; result: boolean }> { - const origConfig = await fetchSiteConfiguration(gqlClient).toPromise() + const origConfig = await lastValueFrom(fetchSiteConfiguration(gqlClient)) let newContents = origConfig.configuration.effectiveContents for (const editFunc of edits) { newContents = jsonc.applyEdits(newContents, editFunc(newContents)) } return { - result: await updateSiteConfiguration(gqlClient, origConfig.configuration.id, newContents).toPromise(), + result: await lastValueFrom(updateSiteConfiguration(gqlClient, origConfig.configuration.id, newContents)), destroy: async () => { - const site = await fetchSiteConfiguration(gqlClient).toPromise() + const site = await lastValueFrom(fetchSiteConfiguration(gqlClient)) await updateSiteConfiguration( gqlClient, site.configuration.id, diff --git a/client/web/src/repo/RepoContainer.tsx b/client/web/src/repo/RepoContainer.tsx index 65ab0edec1d..42129a3b05e 100644 --- a/client/web/src/repo/RepoContainer.tsx +++ b/client/web/src/repo/RepoContainer.tsx @@ -175,7 +175,7 @@ export const RepoContainer: FC = props => { } if (isCloneInProgressErrorLike(error)) { - return of(asError(error)) + return of(asError(error)) } throw error @@ -185,7 +185,7 @@ export const RepoContainer: FC = props => { ) .pipe( repeatUntil(value => !isCloneInProgressErrorLike(value), { delay: 1000 }), - catchError(error => of(asError(error))) + catchError(error => of(asError(error))) ), [repoName, revision] ) diff --git a/client/web/src/repo/blame/useBlameHunks.ts b/client/web/src/repo/blame/useBlameHunks.ts index 9cabd8abcbb..c7260b67fdb 100644 --- a/client/web/src/repo/blame/useBlameHunks.ts +++ b/client/web/src/repo/blame/useBlameHunks.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source' import { formatDistanceStrict } from 'date-fns' import { truncate } from 'lodash' -import { Observable, of } from 'rxjs' +import { Observable, lastValueFrom, of } from 'rxjs' import { catchError, map, throttleTime } from 'rxjs/operators' import { type ErrorLike, memoizeObservable } from '@sourcegraph/common' @@ -192,25 +192,25 @@ const fetchBlameViaStreaming = memoizeObservable( ) async function fetchRepositoryData(repoName: string): Promise> { - return requestGraphQL( - gql` - query FirstCommitDate($repo: String!) { - repository(name: $repo) { - firstEverCommit { - author { - date + return lastValueFrom( + requestGraphQL( + gql` + query FirstCommitDate($repo: String!) { + repository(name: $repo) { + firstEverCommit { + author { + date + } + } + externalURLs { + url + serviceKind } } - externalURLs { - url - serviceKind - } } - } - `, - { repo: repoName } - ) - .pipe( + `, + { repo: repoName } + ).pipe( map(dataOrThrowErrors), map(({ repository }) => { const firstCommitDate = repository?.firstEverCommit?.author?.date @@ -220,7 +220,7 @@ async function fetchRepositoryData(repoName: string): Promise { - private toggles = new Subject() + private toggles = new Subject() private subscriptions = new Subscription() /** diff --git a/client/web/src/repo/settings/RepoSettingsArea.tsx b/client/web/src/repo/settings/RepoSettingsArea.tsx index 84aff5f45c7..8823b18474b 100644 --- a/client/web/src/repo/settings/RepoSettingsArea.tsx +++ b/client/web/src/repo/settings/RepoSettingsArea.tsx @@ -7,7 +7,7 @@ import { Routes, Route } from 'react-router-dom' import { of } from 'rxjs' import { catchError } from 'rxjs/operators' -import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common' +import { asError, isErrorLike } from '@sourcegraph/common' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { useObservable, ErrorMessage } from '@sourcegraph/wildcard' @@ -46,10 +46,7 @@ export const RepoSettingsArea: React.FunctionComponent { const repoName = props.repoName const repoOrError = useObservable( - useMemo( - () => fetchSettingsAreaRepository(repoName).pipe(catchError(error => of(asError(error)))), - [repoName] - ) + useMemo(() => fetchSettingsAreaRepository(repoName).pipe(catchError(error => of(asError(error)))), [repoName]) ) useBreadcrumb(useMemo(() => ({ key: 'settings', element: 'Settings' }), [])) diff --git a/client/web/src/site-admin/SiteAdminConfigurationPage.tsx b/client/web/src/site-admin/SiteAdminConfigurationPage.tsx index 769c5f32956..cf77d12001e 100644 --- a/client/web/src/site-admin/SiteAdminConfigurationPage.tsx +++ b/client/web/src/site-admin/SiteAdminConfigurationPage.tsx @@ -4,7 +4,7 @@ import type { FC } from 'react' import { type ApolloClient, useApolloClient } from '@apollo/client' import classNames from 'classnames' import * as jsonc from 'jsonc-parser' -import { Subject, Subscription } from 'rxjs' +import { lastValueFrom, Subject, Subscription } from 'rxjs' import { delay, mergeMap, retryWhen, tap, timeout } from 'rxjs/operators' import { logger } from '@sourcegraph/common' @@ -468,7 +468,7 @@ class SiteAdminConfigurationContent extends React.Component { let restartToApply = false try { - restartToApply = await updateSiteConfiguration(lastConfigurationID, newContents).toPromise() + restartToApply = await lastValueFrom(updateSiteConfiguration(lastConfigurationID, newContents)) } catch (error) { logger.error(error) this.setState({ @@ -512,7 +512,7 @@ class SiteAdminConfigurationContent extends React.Component { this.setState({ restartToApply }) try { - const site = await fetchSite().toPromise() + const site = await lastValueFrom(fetchSite()) this.setState({ site, diff --git a/client/web/src/site-admin/UserManagement/components/useUserListActions.tsx b/client/web/src/site-admin/UserManagement/components/useUserListActions.tsx index 7478f4d1947..be38856c2b0 100644 --- a/client/web/src/site-admin/UserManagement/components/useUserListActions.tsx +++ b/client/web/src/site-admin/UserManagement/components/useUserListActions.tsx @@ -1,5 +1,7 @@ import React, { useState, useCallback } from 'react' +import { lastValueFrom } from 'rxjs' + import { logger } from '@sourcegraph/common' import { useMutation } from '@sourcegraph/http-client' import { Text } from '@sourcegraph/wildcard' @@ -202,8 +204,7 @@ export function useUserListActions(onEnd: (error?: any) => void): UseUserListAct const handleResetUserPassword = useCallback( ([user]: SiteUser[]) => { if (confirm('Are you sure you want to reset the selected user password?')) { - randomizeUserPassword(user.id) - .toPromise() + lastValueFrom(randomizeUserPassword(user.id)) .then(({ resetPasswordURL, emailSent }) => { if (resetPasswordURL === null || emailSent) { createOnSuccess( diff --git a/client/web/src/tour/components/Tour/utils.tsx b/client/web/src/tour/components/Tour/utils.tsx index 4b729c83711..e2a207535d7 100644 --- a/client/web/src/tour/components/Tour/utils.tsx +++ b/client/web/src/tour/components/Tour/utils.tsx @@ -1,6 +1,6 @@ import isAbsoluteURL from 'is-absolute-url' import { memoize, noop } from 'lodash' -import { type Subscriber, type Subscription, fromEvent, of } from 'rxjs' +import { type Subscriber, type Subscription, fromEvent, of, lastValueFrom } from 'rxjs' import { map } from 'rxjs/operators' import { @@ -77,22 +77,22 @@ const firstMatchMessageHandlers: MessageHandlers = { */ const fetchStreamSuggestions = memoize( (query: string, sourcegraphURL?: string): Promise => - search( - of(query), - { - version: LATEST_VERSION, - patternType: SearchPatternType.standard, - caseSensitive: false, - trace: undefined, - sourcegraphURL, - }, - firstMatchMessageHandlers - ) - .pipe( + lastValueFrom( + search( + of(query), + { + version: LATEST_VERSION, + patternType: SearchPatternType.standard, + caseSensitive: false, + trace: undefined, + sourcegraphURL, + }, + firstMatchMessageHandlers + ).pipe( switchAggregateSearchResults, map(suggestions => suggestions.results) ) - .toPromise(), + ), (query, sourcegraphURL) => `${query}|${sourcegraphURL}` ) diff --git a/client/web/src/user/settings/auth/RemoveExternalAccountModal.tsx b/client/web/src/user/settings/auth/RemoveExternalAccountModal.tsx index 3318f5d300b..4988c5d5bdc 100644 --- a/client/web/src/user/settings/auth/RemoveExternalAccountModal.tsx +++ b/client/web/src/user/settings/auth/RemoveExternalAccountModal.tsx @@ -1,5 +1,7 @@ import React, { useCallback, useState } from 'react' +import { lastValueFrom } from 'rxjs' + import { asError, type ErrorLike } from '@sourcegraph/common' import { gql, dataOrThrowErrors } from '@sourcegraph/http-client' import { Button, Modal, H3, Form } from '@sourcegraph/wildcard' @@ -9,16 +11,18 @@ import type { Scalars, DeleteExternalAccountResult, DeleteExternalAccountVariabl const deleteUserExternalAccount = async (externalAccount: Scalars['ID']): Promise => { dataOrThrowErrors( - await requestGraphQL( - gql` - mutation DeleteExternalAccount($externalAccount: ID!) { - deleteExternalAccount(externalAccount: $externalAccount) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation DeleteExternalAccount($externalAccount: ID!) { + deleteExternalAccount(externalAccount: $externalAccount) { + alwaysNil + } } - } - `, - { externalAccount } - ).toPromise() + `, + { externalAccount } + ) + ) ) } diff --git a/client/web/src/user/settings/backend.tsx b/client/web/src/user/settings/backend.tsx index bfcda808962..1b935541d9d 100644 --- a/client/web/src/user/settings/backend.tsx +++ b/client/web/src/user/settings/backend.tsx @@ -1,4 +1,4 @@ -import { EMPTY, type Observable, Subject } from 'rxjs' +import { EMPTY, type Observable, Subject, lastValueFrom } from 'rxjs' import { bufferTime, catchError, concatMap, map } from 'rxjs/operators' import { createAggregateError } from '@sourcegraph/common' @@ -126,10 +126,11 @@ export const logEventsMutation = gql` ` function sendEvents(events: Event[]): Promise { - return requestGraphQL(logEventsMutation, { - events, - }) - .toPromise() + return lastValueFrom( + requestGraphQL(logEventsMutation, { + events, + }) + ) .then(dataOrThrowErrors) .then(() => {}) } diff --git a/client/web/src/user/settings/emails/AddUserEmailForm.tsx b/client/web/src/user/settings/emails/AddUserEmailForm.tsx index b6a0627e6ac..55629a6580e 100644 --- a/client/web/src/user/settings/emails/AddUserEmailForm.tsx +++ b/client/web/src/user/settings/emails/AddUserEmailForm.tsx @@ -1,6 +1,7 @@ import React, { type FunctionComponent, useMemo, useState } from 'react' import classNames from 'classnames' +import { lastValueFrom } from 'rxjs' import { asError, isErrorLike, type ErrorLike } from '@sourcegraph/common' import { gql, dataOrThrowErrors } from '@sourcegraph/http-client' @@ -55,16 +56,18 @@ export const AddUserEmailForm: FunctionComponent> try { dataOrThrowErrors( - await requestGraphQL( - gql` - mutation AddUserEmail($user: ID!, $email: String!) { - addUserEmail(user: $user, email: $email) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation AddUserEmail($user: ID!, $email: String!) { + addUserEmail(user: $user, email: $email) { + alwaysNil + } } - } - `, - { user: user.id, email: emailState.value } - ).toPromise() + `, + { user: user.id, email: emailState.value } + ) + ) ) eventLogger.log('NewUserEmailAddressAdded') diff --git a/client/web/src/user/settings/emails/SetUserPrimaryEmailForm.tsx b/client/web/src/user/settings/emails/SetUserPrimaryEmailForm.tsx index f71d501c654..384b1227325 100644 --- a/client/web/src/user/settings/emails/SetUserPrimaryEmailForm.tsx +++ b/client/web/src/user/settings/emails/SetUserPrimaryEmailForm.tsx @@ -1,6 +1,7 @@ import React, { useState, type FunctionComponent, useCallback } from 'react' import classNames from 'classnames' +import { lastValueFrom } from 'rxjs' import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common' import { gql, dataOrThrowErrors } from '@sourcegraph/http-client' @@ -58,16 +59,18 @@ export const SetUserPrimaryEmailForm: FunctionComponent( - gql` - mutation SetUserEmailPrimary($user: ID!, $email: String!) { - setUserEmailPrimary(user: $user, email: $email) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation SetUserEmailPrimary($user: ID!, $email: String!) { + setUserEmailPrimary(user: $user, email: $email) { + alwaysNil + } } - } - `, - { user: user.id, email: primaryEmail } - ).toPromise() + `, + { user: user.id, email: primaryEmail } + ) + ) ) eventLogger.log('UserEmailAddressSetAsPrimary') diff --git a/client/web/src/user/settings/emails/UserEmail.tsx b/client/web/src/user/settings/emails/UserEmail.tsx index 83693122be8..c4ba19161e9 100644 --- a/client/web/src/user/settings/emails/UserEmail.tsx +++ b/client/web/src/user/settings/emails/UserEmail.tsx @@ -1,5 +1,7 @@ import { type FunctionComponent, useState, useCallback } from 'react' +import { lastValueFrom } from 'rxjs' + import { asError, type ErrorLike } from '@sourcegraph/common' import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' import { Badge, Button, screenReaderAnnounce } from '@sourcegraph/wildcard' @@ -35,16 +37,18 @@ export const resendVerificationEmail = async ( ): Promise => { try { dataOrThrowErrors( - await requestGraphQL( - gql` - mutation ResendVerificationEmail($userID: ID!, $email: String!) { - resendVerificationEmail(user: $userID, email: $email) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation ResendVerificationEmail($userID: ID!, $email: String!) { + resendVerificationEmail(user: $userID, email: $email) { + alwaysNil + } } - } - `, - { userID, email } - ).toPromise() + `, + { userID, email } + ) + ) ) eventLogger.log('UserEmailAddressVerificationResent') @@ -79,16 +83,18 @@ export const UserEmail: FunctionComponent> = ({ try { dataOrThrowErrors( - await requestGraphQL( - gql` - mutation RemoveUserEmail($user: ID!, $email: String!) { - removeUserEmail(user: $user, email: $email) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation RemoveUserEmail($user: ID!, $email: String!) { + removeUserEmail(user: $user, email: $email) { + alwaysNil + } } - } - `, - { user, email } - ).toPromise() + `, + { user, email } + ) + ) ) setIsLoading(false) @@ -108,16 +114,18 @@ export const UserEmail: FunctionComponent> = ({ try { dataOrThrowErrors( - await requestGraphQL( - gql` - mutation SetUserEmailVerified($user: ID!, $email: String!, $verified: Boolean!) { - setUserEmailVerified(user: $user, email: $email, verified: $verified) { - alwaysNil + await lastValueFrom( + requestGraphQL( + gql` + mutation SetUserEmailVerified($user: ID!, $email: String!, $verified: Boolean!) { + setUserEmailVerified(user: $user, email: $email, verified: $verified) { + alwaysNil + } } - } - `, - { user, email, verified } - ).toPromise() + `, + { user, email, verified } + ) + ) ) setIsLoading(false) diff --git a/client/web/src/user/settings/emails/UserSettingsEmailsPage.tsx b/client/web/src/user/settings/emails/UserSettingsEmailsPage.tsx index 2655be2fc5c..879ff7943be 100644 --- a/client/web/src/user/settings/emails/UserSettingsEmailsPage.tsx +++ b/client/web/src/user/settings/emails/UserSettingsEmailsPage.tsx @@ -1,6 +1,7 @@ import React, { type FunctionComponent, useEffect, useState, useCallback } from 'react' import classNames from 'classnames' +import { lastValueFrom } from 'rxjs' import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common' import { gql, dataOrThrowErrors, useQuery } from '@sourcegraph/http-client' @@ -142,27 +143,29 @@ export const UserSettingsEmailsPage: FunctionComponent { return dataOrThrowErrors( - await requestGraphQL( - gql` - fragment UserEmail on UserEmail { - email - isPrimary - verified - verificationPending - viewerCanManuallyVerify - } - query UserEmails($user: ID!) { - node(id: $user) { - ... on User { - __typename - emails { - ...UserEmail + await lastValueFrom( + requestGraphQL( + gql` + fragment UserEmail on UserEmail { + email + isPrimary + verified + verificationPending + viewerCanManuallyVerify + } + query UserEmails($user: ID!) { + node(id: $user) { + ... on User { + __typename + emails { + ...UserEmail + } } } } - } - `, - { user: userID } - ).toPromise() + `, + { user: userID } + ) + ) ) } diff --git a/package.json b/package.json index 07d26f7b317..ba3039986b9 100644 --- a/package.json +++ b/package.json @@ -396,7 +396,7 @@ "recharts": "^1.8.5", "regexpp": "^3.1.0", "resize-observer-polyfill": "^1.5.1", - "rxjs": "^6.6.3", + "rxjs": "^7.8.1", "semver": "^7.3.2", "stream-http": "^3.2.0", "stream-json": "^1.7.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87cf53d056b..465f4d980c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -407,8 +407,8 @@ importers: specifier: ^1.5.1 version: 1.5.1 rxjs: - specifier: ^6.6.3 - version: 6.6.7 + specifier: ^7.8.1 + version: 7.8.1 semver: specifier: ^7.3.2 version: 7.6.0 @@ -22898,6 +22898,7 @@ packages: engines: {npm: '>=2.0.0'} dependencies: tslib: 2.1.0 + dev: true /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}