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](da5ddc99b6/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.
This commit is contained in:
Felix Kling 2024-03-18 14:02:57 +01:00 committed by GitHub
parent c134ad7a1c
commit c529631483
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 910 additions and 914 deletions

View File

@ -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<void> {
variables: V
sourcegraphURL?: string
}): Promise<GraphQLResult<T>> {
return requestGraphQL<T, V>({ request, variables, sourcegraphURL }).toPromise()
return lastValueFrom(requestGraphQL<T, V>({ request, variables, sourcegraphURL }))
},
async notifyRepoSyncError({ sourcegraphURL, hasRepoSyncError }, sender: browser.runtime.MessageSender) {

View File

@ -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<Target>(driver.browser, 'targetcreated')
.pipe(
firstValueFrom(
fromEvent<Target>(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 {

View File

@ -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<browser.omnibox.SuggestResult[]> => {
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<void> => {
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<ErrorLike | Settings, ErrorLike>(isErrorLike)(settings)) {
this.defaultPatternType =

View File

@ -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<CodeHost>()({
),
prepareCodeHost: async requestGraphQL =>
requestGraphQL<ResolveRepoNameResult, ResolveRepoNameVariables>({
request: gql`
query ResolveRepoName($cloneURL: String!) {
repository(cloneURL: $cloneURL) {
name
lastValueFrom(
requestGraphQL<ResolveRepoNameResult, ResolveRepoNameVariables>({
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(),
),
})

View File

@ -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<string> {
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(

View File

@ -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', () => {

View File

@ -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) {

View File

@ -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)

View File

@ -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<MutationRecordLike[]> = 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<HTMLElement>('#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<HTMLElement>('#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<HTMLElement>('#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<MutationRecordLike[]> = 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<HTMLElement>('#view1')!] }],
[{ addedNodes: [], removedNodes: [document.querySelector<HTMLElement>('#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<void>(resolve => view1.subscriptions.add(resolve))
const v3Removed = new Promise<void>(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<HTMLElement>('#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<void>(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<void>(resolve => view.subscriptions.add(resolve))
mutations.next([{ addedNodes: [], removedNodes: [testElement] }])
await unsubscribed
})

View File

@ -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<void> {
try {
const event = await fromEvent<MessageEvent>(self, 'message').pipe(take(1)).toPromise()
const event = await firstValueFrom(fromEvent<MessageEvent>(self, 'message'))
if (!isInitMessage(event.data)) {
throw new Error('First message event in extension host worker was not a well-formed InitMessage')
}

View File

@ -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)) {

View File

@ -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'),
}

View File

@ -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.

View File

@ -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<unknown> =>
* single result, to the same type.
*/
export const toMaybeLoadingProviderResult = <T>(
value: Subscribable<MaybeLoadingResult<T>> | PromiseLike<T>
value: Observable<MaybeLoadingResult<T>> | PromiseLike<T>
): Observable<MaybeLoadingResult<T>> =>
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`.

View File

@ -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()
})

View File

@ -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<C extends object, D, A> {
/**
* 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<C extends object, D, A> {
pinOptions?: {
/** Emit on this Observable to pin the popover. */
pins: Subscribable<void>
pins: ObservableInput<void>
/** * Emit on this Observable when the close button in the HoverOverlay was clicked */
closeButtonClicks: Subscribable<void>
closeButtonClicks: ObservableInput<void>
}
hoverOverlayElements: Subscribable<HTMLElement | null>
hoverOverlayElements: ObservableInput<HTMLElement | null>
/**
* Called to get the data to display in the hover.
@ -199,7 +198,7 @@ export interface AdjustPositionProps<C extends object> {
*
* @template C Extra context for the hovered token.
*/
export type PositionAdjuster<C extends object> = (props: AdjustPositionProps<C>) => SubscribableOrPromise<Position>
export type PositionAdjuster<C extends object> = (props: AdjustPositionProps<C>) => ObservableInput<Position>
/**
* HoverifyOptions that need to be included internally with every event
@ -235,13 +234,13 @@ export interface EventOptions<C extends object> {
*/
export interface HoverifyOptions<C extends object>
extends Pick<EventOptions<C>, Exclude<keyof EventOptions<C>, 'codeViewId'>> {
positionEvents: Subscribable<PositionEvent>
positionEvents: ObservableInput<PositionEvent>
/**
* 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<PositionJump>
positionJumps?: ObservableInput<PositionJump>
}
/**
@ -388,7 +387,7 @@ export const MOUSEOVER_DELAY = 50
*/
export type HoverProvider<C extends object, D> = (
position: HoveredToken & C
) => Subscribable<MaybeLoadingResult<(HoverAttachment & D) | null>> | PromiseLike<(HoverAttachment & D) | null>
) => Observable<MaybeLoadingResult<(HoverAttachment & D) | null>> | 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<C extends object, D> = (
*/
export type DocumentHighlightProvider<C extends object> = (
position: HoveredToken & C
) => Subscribable<DocumentHighlight[]> | PromiseLike<DocumentHighlight[]>
) => ObservableInput<DocumentHighlight[]>
/**
* @template C Extra context for the hovered token.
* @template A The type of an action.
*/
export type ActionsProvider<C extends object, A> = (position: HoveredToken & C) => SubscribableOrPromise<A[] | null>
export type ActionsProvider<C extends object, A> = (position: HoveredToken & C) => ObservableInput<A[] | null>
/**
* Function responsible for resolving the position of a hovered token
@ -1050,7 +1049,9 @@ export function createHoverifier<C extends object, D, A>({
}
// 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(

View File

@ -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<HTMLElement>): Observable<PositionEvent> =>
(elements: Observable<HTMLElement>): Observable<PositionEvent> =>
merge(
from(elements).pipe(
switchMap(element =>

View File

@ -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<HoverAttachment> = {},
delayTime?: number
): HoverProvider<{}, {}> {
return () =>
of<MaybeLoadingResult<{}>>({ 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<DocumentHighlight>[] = [],
delayTime?: number
): DocumentHighlightProvider<{}> {
return () => of<DocumentHighlight[]>(documentHighlights.map(createDocumentHighlight)).pipe(delay(delayTime ?? 0))
return () => of(documentHighlights.map(createDocumentHighlight)).pipe(delay(delayTime ?? 0))
}
/**

View File

@ -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",
],
)

View File

@ -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<T>(observables: ObservableInput<T>[], def
return zip(...observables)
}
default: {
return new Observable<T[]>(subscribeToArray(observables)).lift(new CombineLatestOperator(defaultValue))
}
}
}
return new Observable<T[]>(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<T> implements Operator<T, T[]> {
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<T[]>, 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<T> extends OuterSubscriber<T, T[]> {
private activeObservables = 0
private values: any[] = []
private observables: Observable<any>[] = []
private scheduled = false
constructor(observer: PartialObserver<T[]>, 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
})
}
}

View File

@ -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<T>(value: Subscribable<T>): Observable<T> {
if (isObservable(value)) {
// type casting should be fine since we already know that
// value is at least a Subscribable<T>
return value as Observable<T>
}
return new Observable<T>(subscriber => value.subscribe(subscriber))
}

View File

@ -2,3 +2,4 @@ export * from './asObservable'
export * from './combineLatestOrDefault'
export * from './memoizeObservable'
export * from './repeatUntil'
export * from './fromSubscribable'

View File

@ -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<SiteVersionAndCurrentAuthStateResult, CurrentAuthStateVariables>({
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<SiteVersionAndCurrentAuthStateResult, CurrentAuthStateVariables>({
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 }
}

View File

@ -82,12 +82,12 @@ export interface ActionsNavItemsProps
export const ActionsNavItems: React.FunctionComponent<React.PropsWithChildren<ActionsNavItemsProps>> = props => {
const { scope, extraContext, extensionsController, menu, wrapInList, transformContributions = identity } = props
const scopeChanges = useMemo(() => new ReplaySubject<ContributionScope>(1), [])
const scopeChanges = useMemo(() => new ReplaySubject<ContributionScope | undefined>(1), [])
useDeepCompareEffectNoCheck(() => {
scopeChanges.next(scope)
}, [scope])
const extraContextChanges = useMemo(() => new ReplaySubject<Context<unknown>>(1), [])
const extraContextChanges = useMemo(() => new ReplaySubject<Context<unknown> | undefined>(1), [])
useDeepCompareEffectNoCheck(() => {
extraContextChanges.next(extraContext)
}, [extraContext])

View File

@ -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 = <T>(
const observable = from(
isPromiseLike(proxyOrProxyPromise) ? proxyOrProxyPromise : Promise.resolve(proxyOrProxyPromise)
).pipe(
mergeMap((proxySubscribable): Subscribable<ProxyOrClone<T>> => {
mergeMap((proxySubscribable): Observable<ProxyOrClone<T>> => {
proxySubscription.add(new ProxySubscription(proxySubscribable))
return {
// Needed for Rx type check
[symbolObservable](): Subscribable<ProxyOrClone<T>> {
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<typeof proxySubscribable['subscribe']>[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<typeof proxySubscribable['subscribe']>[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 })

View File

@ -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<ExtensionHostAPIFactory>(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?

View File

@ -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,

View File

@ -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) => {

View File

@ -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<void> {
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)')
}

View File

@ -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'

View File

@ -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<T, R = T>(
mapFunc: (value: T | undefined | null) => R = identity
): Observable<R> {
let observable: Observable<R>
if (result && (isPromiseLike(result) || isObservable<T>(result) || isSubscribable(result))) {
observable = from(result).pipe(map(mapFunc))
if (result && (isPromiseLike(result) || isObservable(result))) {
observable = from(result as ObservableInput<T>).pipe(map(mapFunc))
} else if (isSubscribable(result)) {
observable = fromSubscribable(result as Subscribable<T>).pipe(map(mapFunc))
} else if (isAsyncIterable(result)) {
observable = observableFromAsyncIterable(result).pipe(map(mapFunc))
} else {

View File

@ -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<Context<unknown>>,
state.context as Observable<Context<unknown>>,
]).pipe(
map(([multiContributions, activeEditor, settings, context]) => {
// Merge in extra context.
@ -431,7 +431,7 @@ export function callProviders<TRegisteredProvider, TProviderResult, TMergedResul
concat(
[LOADING],
providerResultToObservable(safeInvokeProvider(provider)).pipe(
defaultIfEmpty<typeof LOADING | TProviderResult | null | undefined>(null),
defaultIfEmpty(null),
catchError(error => {
logError(error)
return [null]
@ -443,7 +443,7 @@ export function callProviders<TRegisteredProvider, TProviderResult, TMergedResul
)
)
.pipe(
defaultIfEmpty<(typeof LOADING | TProviderResult | null | undefined)[]>([]),
defaultIfEmpty([]),
map(results => ({
isLoading: results.some(hover => hover === LOADING),
result: mergeResult(results),

View File

@ -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

View File

@ -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' }, [])

View File

@ -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' })

View File

@ -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({

View File

@ -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 = [

View File

@ -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' })

View File

@ -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'

View File

@ -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<clientType.Location[]>
getImplementations(parameters: TextDocumentPositionParameters): Promise<clientType.Location[]>
getHover(textParameters: TextDocumentPositionParameters): Promise<HoverMerged | null | undefined>
getHover(textParameters: TextDocumentPositionParameters): Promise<HoverMerged | null>
getDocumentHighlights(textParameters: TextDocumentPositionParameters): Promise<DocumentHighlight[]>
}
@ -57,9 +57,9 @@ export async function getOrCreateCodeIntelAPI(context: PlatformContext): Promise
return codeIntelAPI
}
return new Promise<CodeIntelAPI>((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<sourcegraph.Definition>
): Promise<clientType.Location[]> {
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<boolean> {
@ -128,26 +127,26 @@ class DefaultCodeIntelAPI implements CodeIntelAPI {
request.providers.implementations.provideLocations(request.document, request.position)
)
}
public getHover(textParameters: TextDocumentPositionParameters): Promise<HoverMerged | null | undefined> {
public getHover(textParameters: TextDocumentPositionParameters): Promise<HoverMerged | null> {
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<DocumentHighlight[]> {
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<T>(promise: Observable<T>): Observable<MaybeLoadingResult<T>> {
return promise.pipe(
map(result => {
const maybeLoadingResult: MaybeLoadingResult<T> = { isLoading: false, result }
return maybeLoadingResult
})
)
function thenMaybeLoadingResult<T>(result: T): MaybeLoadingResult<T> {
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))),
}
}

View File

@ -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<T>(query: string, vars?: { [name: string]: unknow
)
)
}
return context
.requestGraphQL<T, any>({ request: query, variables: vars as any, mightContainPrivateInfo: true })
.toPromise()
return lastValueFrom(
context.requestGraphQL<T, any>({ request: query, variables: vars as any, mightContainPrivateInfo: true })
)
}
export function getSetting<T>(key: string): T | undefined {

View File

@ -396,7 +396,7 @@ describe('getDefinitionURL', () => {
Partial<ViewStateSpec>
) => ''
)
await of<MaybeLoadingResult<Location[]>>({
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<MaybeLoadingResult<Location[]>>({
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<MaybeLoadingResult<Location[]>>({
of({
isLoading: false,
result: [FIXTURE_LOCATION_CLIENT],
})
@ -464,7 +464,7 @@ describe('getDefinitionURL', () => {
it('emits the definition URL without range', () =>
expect(
of<MaybeLoadingResult<Location[]>>({
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<MaybeLoadingResult<Location[]>>({
of({
isLoading: false,
result: [FIXTURE_LOCATION_CLIENT, { ...FIXTURE_LOCATION, uri: 'other' }],
})

View File

@ -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<MaybeLoadingResult<UIDefinitionURL | null>>
> => {
if (definitions.length === 0) {
return of<MaybeLoadingResult<UIDefinitionURL | null>>({ 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<MaybeLoadingResult<UIDefinitionURL | null>>({
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.')

View File

@ -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<SettingsCascadeOrError<Settings>>
readonly settings: Observable<SettingsCascadeOrError<Settings>>
/**
* Update the settings for the subject, either by inserting/changing a specific value or by overwriting the

View File

@ -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> | void,
operationName: O
): Promise<Parameters<TGraphQlOperations[O]>[0]> => {
const requestPromise = graphQlRequests
.pipe(
const requestPromise = lastValueFrom(
graphQlRequests.pipe(
first(
(request: GraphQLRequestEvent<TGraphQlOperationNames>): request is GraphQLRequestEvent<O> =>
request.operationName === operationName
),
timeoutWith(4000, throwError(new Error(`Timeout waiting for GraphQL request "${operationName}"`)))
)
.toPromise()
)
await triggerRequest()
const { variables } = await requestPromise
return variables

View File

@ -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<T>(subscribable: Subscribable<T>): T[] {
export function collectSubscribableValues<T>(observable: Observable<T>): 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
}

View File

@ -180,7 +180,7 @@ export function createValidationPipeline(
combineLatest([
// Validate immediately if the user has provided an initial input value
concat(
initialValue !== undefined ? of<InputValidationEvent>({ value: initialValue, validate: true }) : EMPTY,
initialValue !== undefined ? of({ value: initialValue, validate: true }) : EMPTY,
inputValidationEvents
),
inputReferences,

View File

@ -206,7 +206,7 @@ export function infinityQuery<TData = any, TVariables extends AnyVariables = Any
'query',
createRequest(args.query, { ...initialVariables, ...variables })
)
return concat<Partial<OperationResultState<TData, TVariables>>>(
return concat(
of({ fetching: true, stale: false, restoring: false }),
from(args.client.executeRequestOperation(operation).toPromise()).pipe(
map(({ data, stale, operation, error, extensions }) => ({

View File

@ -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.
{

View File

@ -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",

View File

@ -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

View File

@ -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<SearchQueryStateObserverProps> = 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) {

View File

@ -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<UpdateExternalServiceResult['updateExternalService']> {
return requestGraphQL<UpdateExternalServiceResult, UpdateExternalServiceVariables>(
UPDATE_EXTERNAL_SERVICE,
variables
)
.pipe(
return lastValueFrom(
requestGraphQL<UpdateExternalServiceResult, UpdateExternalServiceVariables>(
UPDATE_EXTERNAL_SERVICE,
variables
).pipe(
map(dataOrThrowErrors),
map(data => data.updateExternalService)
)
.toPromise()
)
}
export async function deleteExternalService(externalService: Scalars['ID']): Promise<void> {
const result = await requestGraphQL<DeleteExternalServiceResult, DeleteExternalServiceVariables>(
gql`
mutation DeleteExternalService($externalService: ID!) {
deleteExternalService(externalService: $externalService) {
alwaysNil
const result = await lastValueFrom(
requestGraphQL<DeleteExternalServiceResult, DeleteExternalServiceVariables>(
gql`
mutation DeleteExternalService($externalService: ID!) {
deleteExternalService(externalService: $externalService) {
alwaysNil
}
}
}
`,
{ externalService }
).toPromise()
`,
{ externalService }
)
)
dataOrThrowErrors(result)
}

View File

@ -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<void> {
const result = await requestGraphQL<CloseBatchChangeResult, CloseBatchChangeVariables>(
gql`
mutation CloseBatchChange($batchChange: ID!, $closeChangesets: Boolean) {
closeBatchChange(batchChange: $batchChange, closeChangesets: $closeChangesets) {
id
const result = await lastValueFrom(
requestGraphQL<CloseBatchChangeResult, CloseBatchChangeVariables>(
gql`
mutation CloseBatchChange($batchChange: ID!, $closeChangesets: Boolean) {
closeBatchChange(batchChange: $batchChange, closeChangesets: $closeChangesets) {
id
}
}
}
`,
{ batchChange, closeChangesets }
).toPromise()
`,
{ batchChange, closeChangesets }
)
)
dataOrThrowErrors(result)
}

View File

@ -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<void> {
const result = await requestGraphQL<SyncChangesetResult, SyncChangesetVariables>(
gql`
mutation SyncChangeset($changeset: ID!) {
syncChangeset(changeset: $changeset) {
alwaysNil
const result = await lastValueFrom(
requestGraphQL<SyncChangesetResult, SyncChangesetVariables>(
gql`
mutation SyncChangeset($changeset: ID!) {
syncChangeset(changeset: $changeset) {
alwaysNil
}
}
}
`,
{ changeset }
).toPromise()
`,
{ changeset }
)
)
dataOrThrowErrors(result)
}
export async function reenqueueChangeset(changeset: Scalars['ID']): Promise<ChangesetFields> {
return requestGraphQL<ReenqueueChangesetResult, ReenqueueChangesetVariables>(
gql`
mutation ReenqueueChangeset($changeset: ID!) {
reenqueueChangeset(changeset: $changeset) {
...ChangesetFields
return lastValueFrom(
requestGraphQL<ReenqueueChangesetResult, ReenqueueChangesetVariables>(
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<void> {
const result = await requestGraphQL<DeleteBatchChangeResult, DeleteBatchChangeVariables>(
gql`
mutation DeleteBatchChange($batchChange: ID!) {
deleteBatchChange(batchChange: $batchChange) {
alwaysNil
const result = await lastValueFrom(
requestGraphQL<DeleteBatchChangeResult, DeleteBatchChangeVariables>(
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<string> {
return requestGraphQL<ChangesetDiffResult, ChangesetDiffVariables>(
gql`
query ChangesetDiff($changeset: ID!) {
node(id: $changeset) {
__typename
...ChangesetDiffFields
return lastValueFrom(
requestGraphQL<ChangesetDiffResult, ChangesetDiffVariables>(
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<string
return commits[0].diff
})
)
.toPromise()
)
}
const changesetScheduleEstimateFragment = gql`
@ -703,20 +707,20 @@ const changesetScheduleEstimateFragment = gql`
`
export async function getChangesetScheduleEstimate(changeset: Scalars['ID']): Promise<Scalars['DateTime'] | null> {
return requestGraphQL<ChangesetScheduleEstimateResult, ChangesetScheduleEstimateVariables>(
gql`
query ChangesetScheduleEstimate($changeset: ID!) {
node(id: $changeset) {
__typename
...ChangesetScheduleEstimateFields
return lastValueFrom(
requestGraphQL<ChangesetScheduleEstimateResult, ChangesetScheduleEstimateVariables>(
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<void> {
const result = await requestGraphQL<DetachChangesetsResult, DetachChangesetsVariables>(
gql`
mutation DetachChangesets($batchChange: ID!, $changesets: [ID!]!) {
detachChangesets(batchChange: $batchChange, changesets: $changesets) {
id
const result = await lastValueFrom(
requestGraphQL<DetachChangesetsResult, DetachChangesetsVariables>(
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<void> {
const result = await requestGraphQL<CreateChangesetCommentsResult, CreateChangesetCommentsVariables>(
gql`
mutation CreateChangesetComments($batchChange: ID!, $changesets: [ID!]!, $body: String!) {
createChangesetComments(batchChange: $batchChange, changesets: $changesets, body: $body) {
id
const result = await lastValueFrom(
requestGraphQL<CreateChangesetCommentsResult, CreateChangesetCommentsVariables>(
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<void> {
const result = await requestGraphQL<ReenqueueChangesetsResult, ReenqueueChangesetsVariables>(
gql`
mutation ReenqueueChangesets($batchChange: ID!, $changesets: [ID!]!) {
reenqueueChangesets(batchChange: $batchChange, changesets: $changesets) {
id
const result = await lastValueFrom(
requestGraphQL<ReenqueueChangesetsResult, ReenqueueChangesetsVariables>(
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<void> {
const result = await requestGraphQL<MergeChangesetsResult, MergeChangesetsVariables>(
gql`
mutation MergeChangesets($batchChange: ID!, $changesets: [ID!]!, $squash: Boolean!) {
mergeChangesets(batchChange: $batchChange, changesets: $changesets, squash: $squash) {
id
const result = await lastValueFrom(
requestGraphQL<MergeChangesetsResult, MergeChangesetsVariables>(
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<void> {
const result = await requestGraphQL<CloseChangesetsResult, CloseChangesetsVariables>(
gql`
mutation CloseChangesets($batchChange: ID!, $changesets: [ID!]!) {
closeChangesets(batchChange: $batchChange, changesets: $changesets) {
id
const result = await lastValueFrom(
requestGraphQL<CloseChangesetsResult, CloseChangesetsVariables>(
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<void> {
const result = await requestGraphQL<PublishChangesetsResult, PublishChangesetsVariables>(
gql`
mutation PublishChangesets($batchChange: ID!, $changesets: [ID!]!, $draft: Boolean!) {
publishChangesets(batchChange: $batchChange, changesets: $changesets, draft: $draft) {
id
const result = await lastValueFrom(
requestGraphQL<PublishChangesetsResult, PublishChangesetsVariables>(
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)
}

View File

@ -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<CreateBatchChangeResult['createBatchChange']> =>
requestGraphQL<CreateBatchChangeResult, CreateBatchChangeVariables>(
gql`
mutation CreateBatchChange($batchSpec: ID!, $publicationStates: [ChangesetSpecPublicationStateInput!]) {
createBatchChange(batchSpec: $batchSpec, publicationStates: $publicationStates) {
id
url
lastValueFrom(
requestGraphQL<CreateBatchChangeResult, CreateBatchChangeVariables>(
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<ApplyBatchChangeResult['applyBatchChange']> =>
requestGraphQL<ApplyBatchChangeResult, ApplyBatchChangeVariables>(
gql`
mutation ApplyBatchChange(
$batchSpec: ID!
$batchChange: ID!
$publicationStates: [ChangesetSpecPublicationStateInput!]
) {
applyBatchChange(
batchSpec: $batchSpec
ensureBatchChange: $batchChange
publicationStates: $publicationStates
lastValueFrom(
requestGraphQL<ApplyBatchChangeResult, ApplyBatchChangeVariables>(
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()
)

View File

@ -41,8 +41,8 @@ export const EnqueueForm: FunctionComponent<EnqueueFormProps> = ({
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)

View File

@ -170,7 +170,7 @@ export const CodeIntelPreciseIndexesPage: FunctionComponent<CodeIntelPreciseInde
}, [indexerData?.indexerKeys])
// Poke filtered connection to refresh
const refresh = useMemo(() => new Subject<undefined>(), [])
const refresh = useMemo(() => new Subject<void>(), [])
const querySubject = useMemo(() => new Subject<string>(), [])
// State used to control bulk index selection

View File

@ -122,7 +122,7 @@ async function getLangStats(inputs: GetInsightContentInputs): Promise<Categorica
)
.toPromise()
if (stats.languages.length === 0) {
if (!stats || stats.languages.length === 0) {
throw new Error("We couldn't find the language statistics, try changing the repository.")
}

View File

@ -2,6 +2,7 @@ import React, { useContext, useMemo } from 'react'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
import { lastValueFrom } from 'rxjs'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { PageHeader, Container, Button, LoadingSpinner, useObservable, Link, Tooltip } from '@sourcegraph/wildcard'
@ -40,7 +41,7 @@ export const InsightsDashboardCreationPage: React.FunctionComponent<
throw new Error('You have to specify a dashboard visibility')
}
const createdDashboard = await createDashboard({ name, owners: [owner] }).toPromise()
const createdDashboard = await lastValueFrom(createDashboard({ name, owners: [owner] }))
telemetryService.log('CodeInsightsDashboardCreationPageSubmitClick')

View File

@ -3,6 +3,7 @@ import { type FC, useContext, useMemo } from 'react'
import classNames from 'classnames'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import { useParams, useNavigate } from 'react-router-dom'
import { lastValueFrom } from 'rxjs'
import {
Button,
@ -70,13 +71,15 @@ export const EditDashboardPage: FC = props => {
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}`)
}

View File

@ -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<string> = null
while (true) {
const result: GraphQLResult<RepositoriesByNamesResult> = await requestGraphQL<
RepositoriesByNamesResult,
RepositoriesByNamesVariables
>(query, {
names,
first,
after,
}).toPromise()
const result = await lastValueFrom(
requestGraphQL<RepositoriesByNamesResult, RepositoriesByNamesVariables>(query, {
names,
first,
after,
})
)
const data: RepositoriesByNamesResult = dataOrThrowErrors(result)

View File

@ -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<CreateOrganizationResult['createOrganization']> {
return requestGraphQL<CreateOrganizationResult, CreateOrganizationVariables>(
gql`
mutation CreateOrganization($name: String!, $displayName: String) {
createOrganization(name: $name, displayName: $displayName) {
id
name
settingsURL
return lastValueFrom(
requestGraphQL<CreateOrganizationResult, CreateOrganizationVariables>(
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`

View File

@ -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<InviteUserToOrganizationResult['inviteUserToOrganization']> {
return requestGraphQL<InviteUserToOrganizationResult, InviteUserToOrganizationVariables>(
gql`
mutation InviteUserToOrganization($organization: ID!, $username: String!) {
inviteUserToOrganization(organization: $organization, username: $username) {
...InviteUserToOrganizationFields
return lastValueFrom(
requestGraphQL<InviteUserToOrganizationResult, InviteUserToOrganizationVariables>(
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<void> {

View File

@ -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<ExternalServiceNodeFields[]> {
return gqlClient
.queryGraphQL<ExternalServicesRegressionResult, ExternalServicesRegressionVariables>(
gql`
query ExternalServicesRegression($first: Int) {
externalServices(first: $first) {
nodes {
...ExternalServiceNodeFields
return lastValueFrom(
gqlClient
.queryGraphQL<ExternalServicesRegressionResult, ExternalServicesRegressionVariables>(
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<string> {
return gqlClient
.queryGraphQL<SiteProductVersionResult, SiteProductVersionVariables>(
gql`
query SiteProductVersion {
site {
productVersion
return lastValueFrom(
gqlClient
.queryGraphQL<SiteProductVersionResult, SiteProductVersionVariables>(
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<string>
export function getViewerSettings({
requestGraphQL,
}: Pick<PlatformContext, 'requestGraphQL'>): Promise<ViewerSettingsResult['viewerSettings']> {
return requestGraphQL<ViewerSettingsResult, ViewerSettingsVariables>({
request: viewerSettingsQuery,
variables: {},
mightContainPrivateInfo: true,
})
.pipe(
return lastValueFrom(
requestGraphQL<ViewerSettingsResult, ViewerSettingsVariables>({
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<PlatformContext, 'requestGraphQL'>,
username: string
): Promise<UserResult['user']> {
const user = await requestGraphQL<UserResult, UserVariables>({
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<UserResult, UserVariables>({
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<SearchResult['search']> {
return requestGraphQL<SearchResult, SearchVariables>({
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<SearchResult, SearchVariables>({
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()
)
}
/**

View File

@ -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<ResourceDestructor> {
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<PlatformContext, 'requestGraphQL'>,
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,

View File

@ -175,7 +175,7 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
}
if (isCloneInProgressErrorLike(error)) {
return of<ErrorLike>(asError(error))
return of(asError(error))
}
throw error
@ -185,7 +185,7 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
)
.pipe(
repeatUntil(value => !isCloneInProgressErrorLike(value), { delay: 1000 }),
catchError(error => of<ErrorLike>(asError(error)))
catchError(error => of(asError(error)))
),
[repoName, revision]
)

View File

@ -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<Omit<BlameHunkData, 'current'>> {
return requestGraphQL<FirstCommitDateResult, FirstCommitDateVariables>(
gql`
query FirstCommitDate($repo: String!) {
repository(name: $repo) {
firstEverCommit {
author {
date
return lastValueFrom(
requestGraphQL<FirstCommitDateResult, FirstCommitDateVariables>(
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<Omit<BlameHunkData
}
})
)
.toPromise()
)
}
/**

View File

@ -33,7 +33,7 @@ export class ToggleHistoryPanel extends React.PureComponent<
navigate: NavigateFunction
} & RepoHeaderContext
> {
private toggles = new Subject<boolean>()
private toggles = new Subject<void>()
private subscriptions = new Subscription()
/**

View File

@ -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<React.PropsWithChildren<P
}) => {
const repoName = props.repoName
const repoOrError = useObservable(
useMemo(
() => fetchSettingsAreaRepository(repoName).pipe(catchError(error => of<ErrorLike>(asError(error)))),
[repoName]
)
useMemo(() => fetchSettingsAreaRepository(repoName).pipe(catchError(error => of(asError(error)))), [repoName])
)
useBreadcrumb(useMemo(() => ({ key: 'settings', element: 'Settings' }), []))

View File

@ -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<Props, State> {
let restartToApply = false
try {
restartToApply = await updateSiteConfiguration(lastConfigurationID, newContents).toPromise<boolean>()
restartToApply = await lastValueFrom(updateSiteConfiguration(lastConfigurationID, newContents))
} catch (error) {
logger.error(error)
this.setState({
@ -512,7 +512,7 @@ class SiteAdminConfigurationContent extends React.Component<Props, State> {
this.setState({ restartToApply })
try {
const site = await fetchSite().toPromise()
const site = await lastValueFrom(fetchSite())
this.setState({
site,

View File

@ -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(

View File

@ -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<SearchMatch[]> =>
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}`
)

View File

@ -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<void> => {
dataOrThrowErrors(
await requestGraphQL<DeleteExternalAccountResult, DeleteExternalAccountVariables>(
gql`
mutation DeleteExternalAccount($externalAccount: ID!) {
deleteExternalAccount(externalAccount: $externalAccount) {
alwaysNil
await lastValueFrom(
requestGraphQL<DeleteExternalAccountResult, DeleteExternalAccountVariables>(
gql`
mutation DeleteExternalAccount($externalAccount: ID!) {
deleteExternalAccount(externalAccount: $externalAccount) {
alwaysNil
}
}
}
`,
{ externalAccount }
).toPromise()
`,
{ externalAccount }
)
)
)
}

View File

@ -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<void> {
return requestGraphQL<LogEventsResult, LogEventsVariables>(logEventsMutation, {
events,
})
.toPromise()
return lastValueFrom(
requestGraphQL<LogEventsResult, LogEventsVariables>(logEventsMutation, {
events,
})
)
.then(dataOrThrowErrors)
.then(() => {})
}

View File

@ -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<React.PropsWithChildren<Props>>
try {
dataOrThrowErrors(
await requestGraphQL<AddUserEmailResult, AddUserEmailVariables>(
gql`
mutation AddUserEmail($user: ID!, $email: String!) {
addUserEmail(user: $user, email: $email) {
alwaysNil
await lastValueFrom(
requestGraphQL<AddUserEmailResult, AddUserEmailVariables>(
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')

View File

@ -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<React.PropsWithChildren<
try {
dataOrThrowErrors(
await requestGraphQL<SetUserEmailPrimaryResult, SetUserEmailPrimaryVariables>(
gql`
mutation SetUserEmailPrimary($user: ID!, $email: String!) {
setUserEmailPrimary(user: $user, email: $email) {
alwaysNil
await lastValueFrom(
requestGraphQL<SetUserEmailPrimaryResult, SetUserEmailPrimaryVariables>(
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')

View File

@ -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<void> => {
try {
dataOrThrowErrors(
await requestGraphQL<ResendVerificationEmailResult, ResendVerificationEmailVariables>(
gql`
mutation ResendVerificationEmail($userID: ID!, $email: String!) {
resendVerificationEmail(user: $userID, email: $email) {
alwaysNil
await lastValueFrom(
requestGraphQL<ResendVerificationEmailResult, ResendVerificationEmailVariables>(
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<React.PropsWithChildren<Props>> = ({
try {
dataOrThrowErrors(
await requestGraphQL<RemoveUserEmailResult, RemoveUserEmailVariables>(
gql`
mutation RemoveUserEmail($user: ID!, $email: String!) {
removeUserEmail(user: $user, email: $email) {
alwaysNil
await lastValueFrom(
requestGraphQL<RemoveUserEmailResult, RemoveUserEmailVariables>(
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<React.PropsWithChildren<Props>> = ({
try {
dataOrThrowErrors(
await requestGraphQL<SetUserEmailVerifiedResult, SetUserEmailVerifiedVariables>(
gql`
mutation SetUserEmailVerified($user: ID!, $email: String!, $verified: Boolean!) {
setUserEmailVerified(user: $user, email: $email, verified: $verified) {
alwaysNil
await lastValueFrom(
requestGraphQL<SetUserEmailVerifiedResult, SetUserEmailVerifiedVariables>(
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)

View File

@ -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<React.PropsWithChildren<P
async function fetchUserEmails(userID: Scalars['ID']): Promise<UserEmailsResult> {
return dataOrThrowErrors(
await requestGraphQL<UserEmailsResult, UserEmailsVariables>(
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<UserEmailsResult, UserEmailsVariables>(
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 }
)
)
)
}

View File

@ -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",

View File

@ -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==}