Path: blob/main/components/gitpod-protocol/src/gitpod-service.ts
2498 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import parse from "parse-duration";7import {8User,9WorkspaceInfo,10WorkspaceCreationResult,11WorkspaceInstanceUser,12WorkspaceImageBuild,13AuthProviderInfo,14Token,15UserEnvVarValue,16Configuration,17UserInfo,18GitpodTokenType,19GitpodToken,20AuthProviderEntry,21GuessGitTokenScopesParams,22GuessedGitTokenScopes,23ProjectEnvVar,24PrebuiltWorkspace,25UserSSHPublicKeyValue,26SSHPublicKeyValue,27IDESettings,28EnvVarWithValue,29WorkspaceTimeoutSetting,30WorkspaceContext,31LinkedInProfile,32SuggestedRepository,33} from "./protocol";34import {35Team,36TeamMemberInfo,37TeamMembershipInvite,38Project,39TeamMemberRole,40PrebuildWithStatus,41StartPrebuildResult,42PartialProject,43OrganizationSettings,44} from "./teams-projects-protocol";45import { JsonRpcProxy, JsonRpcServer } from "./messaging/proxy-factory";46import { Disposable, CancellationTokenSource, CancellationToken } from "vscode-jsonrpc";47import { HeadlessLogUrls } from "./headless-workspace-log";48import {49WorkspaceInstance,50WorkspaceInstancePort,51WorkspaceInstancePhase,52WorkspaceInstanceRepoStatus,53} from "./workspace-instance";54import { AdminServer } from "./admin-protocol";55import { Emitter } from "./util/event";56import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./analytics";57import { IDEServer } from "./ide-protocol";58import { ListUsageRequest, ListUsageResponse, CostCenterJSON } from "./usage";59import { SupportedWorkspaceClass } from "./workspace-class";60import { BillingMode } from "./billing-mode";61import { WorkspaceRegion } from "./workspace-cluster";6263export interface GitpodClient {64onInstanceUpdate(instance: WorkspaceInstance): void;65onWorkspaceImageBuildLogs: WorkspaceImageBuild.LogCallback;6667onPrebuildUpdate(update: PrebuildWithStatus): void;6869//#region propagating reconnection to iframe70notifyDidOpenConnection(): void;71notifyDidCloseConnection(): void;72//#endregion73}7475export const GitpodServer = Symbol("GitpodServer");76export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer, IDEServer {77// User related API78getLoggedInUser(): Promise<User>;79updateLoggedInUser(user: Partial<User>): Promise<User>;80sendPhoneNumberVerificationToken(phoneNumber: string): Promise<{ verificationId: string }>;81verifyPhoneNumberVerificationToken(phoneNumber: string, token: string, verificationId: string): Promise<boolean>;82getConfiguration(): Promise<Configuration>;83getToken(query: GitpodServer.GetTokenSearchOptions): Promise<Token | undefined>;84getGitpodTokenScopes(tokenHash: string): Promise<string[]>;85deleteAccount(): Promise<void>;86getClientRegion(): Promise<string | undefined>;8788// Auth Provider API89getAuthProviders(): Promise<AuthProviderInfo[]>;90// user-level91getOwnAuthProviders(): Promise<AuthProviderEntry[]>;92updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;93deleteOwnAuthProvider(params: GitpodServer.DeleteOwnAuthProviderParams): Promise<void>;94// org-level95createOrgAuthProvider(params: GitpodServer.CreateOrgAuthProviderParams): Promise<AuthProviderEntry>;96updateOrgAuthProvider(params: GitpodServer.UpdateOrgAuthProviderParams): Promise<AuthProviderEntry>;97getOrgAuthProviders(params: GitpodServer.GetOrgAuthProviderParams): Promise<AuthProviderEntry[]>;98deleteOrgAuthProvider(params: GitpodServer.DeleteOrgAuthProviderParams): Promise<void>;99// public-api compatibility100/** @deprecated used for public-api compatibility only */101getAuthProvider(id: string): Promise<AuthProviderEntry>;102/** @deprecated used for public-api compatibility only */103deleteAuthProvider(id: string): Promise<void>;104/** @deprecated used for public-api compatibility only */105updateAuthProvider(id: string, update: AuthProviderEntry.UpdateOAuth2Config): Promise<AuthProviderEntry>;106107// Query/retrieve workspaces108getWorkspaces(options: GitpodServer.GetWorkspacesOptions): Promise<WorkspaceInfo[]>;109getWorkspaceOwner(workspaceId: string): Promise<UserInfo | undefined>;110getWorkspaceUsers(workspaceId: string): Promise<WorkspaceInstanceUser[]>;111getSuggestedRepositories(organizationId: string): Promise<SuggestedRepository[]>;112searchRepositories(params: SearchRepositoriesParams): Promise<SuggestedRepository[]>;113/**114* **Security:**115* Sensitive information like an owner token is erased, since it allows access for all team members.116* If you need to access an owner token use `getOwnerToken` instead.117*/118getWorkspace(id: string): Promise<WorkspaceInfo>;119isWorkspaceOwner(workspaceId: string): Promise<boolean>;120getOwnerToken(workspaceId: string): Promise<string>;121getIDECredentials(workspaceId: string): Promise<string>;122123/**124* Creates and starts a workspace for the given context URL.125* @param options GitpodServer.CreateWorkspaceOptions126* @return WorkspaceCreationResult127*/128createWorkspace(options: GitpodServer.CreateWorkspaceOptions): Promise<WorkspaceCreationResult>;129startWorkspace(id: string, options: GitpodServer.StartWorkspaceOptions): Promise<StartWorkspaceResult>;130stopWorkspace(id: string): Promise<void>;131deleteWorkspace(id: string): Promise<void>;132setWorkspaceDescription(id: string, desc: string): Promise<void>;133controlAdmission(id: string, level: GitpodServer.AdmissionLevel): Promise<void>;134resolveContext(contextUrl: string): Promise<WorkspaceContext>;135136updateWorkspaceUserPin(id: string, action: GitpodServer.PinAction): Promise<void>;137sendHeartBeat(options: GitpodServer.SendHeartBeatOptions): Promise<void>;138watchWorkspaceImageBuildLogs(workspaceId: string): Promise<void>;139isPrebuildDone(pwsid: string): Promise<boolean>;140getHeadlessLog(instanceId: string): Promise<HeadlessLogUrls>;141142// Workspace timeout143setWorkspaceTimeout(workspaceId: string, duration: WorkspaceTimeoutDuration): Promise<SetWorkspaceTimeoutResult>;144getWorkspaceTimeout(workspaceId: string): Promise<GetWorkspaceTimeoutResult>;145146// Port management147getOpenPorts(workspaceId: string): Promise<WorkspaceInstancePort[]>;148openPort(workspaceId: string, port: WorkspaceInstancePort): Promise<WorkspaceInstancePort | undefined>;149closePort(workspaceId: string, port: number): Promise<void>;150151updateGitStatus(workspaceId: string, status: Required<WorkspaceInstanceRepoStatus> | undefined): Promise<void>;152153// Workspace env vars154getWorkspaceEnvVars(workspaceId: string): Promise<EnvVarWithValue[]>;155156// User env vars157getAllEnvVars(): Promise<UserEnvVarValue[]>;158setEnvVar(variable: UserEnvVarValue): Promise<void>;159deleteEnvVar(variable: UserEnvVarValue): Promise<void>;160161// User SSH Keys162hasSSHPublicKey(): Promise<boolean>;163getSSHPublicKeys(): Promise<UserSSHPublicKeyValue[]>;164addSSHPublicKey(value: SSHPublicKeyValue): Promise<UserSSHPublicKeyValue>;165deleteSSHPublicKey(id: string): Promise<void>;166167// Teams168getTeam(teamId: string): Promise<Team>;169updateTeam(170teamId: string,171team: Partial<Pick<Team, "name" | "maintenanceMode" | "maintenanceNotification">>,172): Promise<Team>;173getTeams(): Promise<Team[]>;174getTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;175createTeam(name: string): Promise<Team>;176joinTeam(inviteId: string): Promise<Team>;177setTeamMemberRole(teamId: string, userId: string, role: TeamMemberRole): Promise<void>;178removeTeamMember(teamId: string, userId: string): Promise<void>;179getGenericInvite(teamId: string): Promise<TeamMembershipInvite>;180resetGenericInvite(inviteId: string): Promise<TeamMembershipInvite>;181deleteTeam(teamId: string): Promise<void>;182getOrgSettings(orgId: string): Promise<OrganizationSettings>;183updateOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<OrganizationSettings>;184getOrgWorkspaceClasses(orgId: string): Promise<SupportedWorkspaceClass[]>;185186getDefaultWorkspaceImage(params: GetDefaultWorkspaceImageParams): Promise<GetDefaultWorkspaceImageResult>;187188// Dedicated, Dedicated, Dedicated189getOnboardingState(): Promise<GitpodServer.OnboardingState>;190191// Projects192/** @deprecated no-op */193getProviderRepositoriesForUser(194params: GetProviderRepositoriesParams,195cancellationToken?: CancellationToken,196): Promise<ProviderRepository[]>;197createProject(params: CreateProjectParams): Promise<Project>;198deleteProject(projectId: string): Promise<void>;199getTeamProjects(teamId: string): Promise<Project[]>;200getProjectOverview(projectId: string): Promise<Project.Overview | undefined>;201findPrebuilds(params: FindPrebuildsParams): Promise<PrebuildWithStatus[]>;202findPrebuildByWorkspaceID(workspaceId: string): Promise<PrebuiltWorkspace | undefined>;203getPrebuild(prebuildId: string): Promise<PrebuildWithStatus | undefined>;204triggerPrebuild(projectId: string, branchName: string | null): Promise<StartPrebuildResult>;205cancelPrebuild(projectId: string, prebuildId: string): Promise<void>;206updateProjectPartial(partialProject: PartialProject): Promise<void>;207setProjectEnvironmentVariable(208projectId: string,209name: string,210value: string,211censored: boolean,212id?: string,213): Promise<void>;214getProjectEnvironmentVariables(projectId: string): Promise<ProjectEnvVar[]>;215deleteProjectEnvironmentVariable(variableId: string): Promise<void>;216217// Gitpod token218getGitpodTokens(): Promise<GitpodToken[]>;219generateNewGitpodToken(options: GitpodServer.GenerateNewGitpodTokenOptions): Promise<string>;220deleteGitpodToken(tokenHash: string): Promise<void>;221222// misc223/** @deprecated always returns false */224isGitHubAppEnabled(): Promise<boolean>;225/** @deprecated this is a no-op */226registerGithubApp(installationId: string): Promise<void>;227228/**229* Stores a new snapshot for the given workspace and bucketId. Returns _before_ the actual snapshot is done. To wait for that, use `waitForSnapshot`.230* @return the snapshot id231*/232takeSnapshot(options: GitpodServer.TakeSnapshotOptions): Promise<string>;233/**234*235* @param snapshotId236*/237waitForSnapshot(snapshotId: string): Promise<void>;238239/**240* Returns the list of snapshots that exist for a workspace.241*/242getSnapshots(workspaceID: string): Promise<string[]>;243244guessGitTokenScopes(params: GuessGitTokenScopesParams): Promise<GuessedGitTokenScopes>;245246/**247* Stripe/Usage248*/249getStripePublishableKey(): Promise<string>;250findStripeSubscriptionId(attributionId: string): Promise<string | undefined>;251getPriceInformation(attributionId: string): Promise<string | undefined>;252createStripeCustomerIfNeeded(attributionId: string, currency: string): Promise<void>;253createHoldPaymentIntent(254attributionId: string,255): Promise<{ paymentIntentId: string; paymentIntentClientSecret: string }>;256subscribeToStripe(attributionId: string, paymentIntentId: string, usageLimit: number): Promise<number | undefined>;257getStripePortalUrl(attributionId: string): Promise<string>;258getCostCenter(attributionId: string): Promise<CostCenterJSON | undefined>;259setUsageLimit(attributionId: string, usageLimit: number): Promise<void>;260getUsageBalance(attributionId: string): Promise<number>;261isCustomerBillingAddressInvalid(attributionId: string): Promise<boolean>;262263listUsage(req: ListUsageRequest): Promise<ListUsageResponse>;264265getBillingModeForTeam(teamId: string): Promise<BillingMode>;266267getLinkedInClientId(): Promise<string>;268connectWithLinkedIn(code: string): Promise<LinkedInProfile>;269270/**271* Analytics272*/273trackEvent(event: RemoteTrackMessage): Promise<void>;274trackLocation(event: RemotePageMessage): Promise<void>;275identifyUser(event: RemoteIdentifyMessage): Promise<void>;276277/**278* Frontend metrics279*/280reportErrorBoundary(url: string, message: string): Promise<void>;281282getSupportedWorkspaceClasses(): Promise<SupportedWorkspaceClass[]>;283updateWorkspaceTimeoutSetting(setting: Partial<WorkspaceTimeoutSetting>): Promise<void>;284285/**286* getIDToken - doesn't actually do anything, just used to authenticat/authorise287*/288getIDToken(): Promise<void>;289}290291export interface RateLimiterError {292method?: string;293294/**295* Retry after this many seconds, earliest.296* cmp.: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After297*/298retryAfter: number;299}300301export interface GetDefaultWorkspaceImageParams {302// filter with workspaceId (actually we will find with organizationId, and it's a real time finding)303workspaceId?: string;304}305306export type DefaultImageSource =307| "installation" // Source installation means the image comes from Gitpod instance install config308| "organization"; // Source organization means the image comes from Organization settings309310export interface GetDefaultWorkspaceImageResult {311image: string;312source: DefaultImageSource;313}314315export interface CreateProjectParams {316name: string;317cloneUrl: string;318teamId: string;319appInstallationId: string;320}321export interface FindPrebuildsParams {322projectId: string;323branch?: string;324latest?: boolean;325prebuildId?: string;326// default: 30327limit?: number;328}329export interface GetProviderRepositoriesParams {330provider: string;331hints?: { installationId: string } | object;332searchString?: string;333limit?: number;334maxPages?: number;335}336export interface SearchRepositoriesParams {337/** @deprecated unused */338organizationId?: string;339searchString: string;340limit?: number; // defaults to 30341}342export interface ProviderRepository {343name: string;344path?: string;345account: string;346accountAvatarUrl: string;347cloneUrl: string;348updatedAt?: string;349installationId?: number;350installationUpdatedAt?: string;351}352353const WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24;354355export type WorkspaceTimeoutDuration = string;356export namespace WorkspaceTimeoutDuration {357export function validate(duration: string): WorkspaceTimeoutDuration {358duration = duration.toLowerCase();359360try {361// Ensure the duration contains proper units (h, m, s, ms, us, ns)362// This prevents bare numbers like "1" from being accepted363if (!/[a-z]/.test(duration)) {364throw new Error("Invalid duration format");365}366367// Use parse-duration library which supports Go duration format perfectly368// This handles mixed-unit durations like "1h30m", "2h15m", etc.369const milliseconds = parse(duration);370371if (milliseconds === undefined || milliseconds === null) {372throw new Error("Invalid duration format");373}374375// Validate the parsed duration is within limits376const maxMs = WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60 * 60 * 1000;377if (milliseconds > maxMs) {378throw new Error("Workspace inactivity timeout cannot exceed 24h");379}380381if (milliseconds <= 0) {382throw new Error(`Invalid timeout value: ${duration}. Timeout must be greater than 0`);383}384385// Return the original duration string - Go's time.ParseDuration will handle it correctly386return duration;387} catch (error) {388// If it's our validation error, re-throw it389if (error.message.includes("cannot exceed 24h") || error.message.includes("must be greater than 0")) {390throw error;391}392// Otherwise, it's a parsing error from the library393throw new Error(`Invalid timeout format: ${duration}. Use Go duration format (e.g., "30m", "1h30m", "2h")`);394}395}396}397398export const WORKSPACE_TIMEOUT_DEFAULT_SHORT: WorkspaceTimeoutDuration = "30m";399export const WORKSPACE_TIMEOUT_DEFAULT_LONG: WorkspaceTimeoutDuration = "60m";400export const WORKSPACE_TIMEOUT_EXTENDED: WorkspaceTimeoutDuration = "180m";401export const WORKSPACE_LIFETIME_SHORT: WorkspaceTimeoutDuration = "8h";402export const WORKSPACE_LIFETIME_LONG: WorkspaceTimeoutDuration = "36h";403404export const MAX_PARALLEL_WORKSPACES_FREE = 4;405export const MAX_PARALLEL_WORKSPACES_PAID = 16;406407export const createServiceMock = function <C extends GitpodClient, S extends GitpodServer>(408methods: Partial<JsonRpcProxy<S>>,409): GitpodServiceImpl<C, S> {410return new GitpodServiceImpl<C, S>(createServerMock(methods));411};412413export const createServerMock = function <S extends GitpodServer>(methods: Partial<JsonRpcProxy<S>>): JsonRpcProxy<S> {414methods.setClient = methods.setClient || (() => {});415methods.dispose = methods.dispose || (() => {});416return new Proxy<JsonRpcProxy<S>>(methods as any as JsonRpcProxy<S>, {417// @ts-ignore418get: (target: S, property: keyof S) => {419const result = target[property];420if (!result) {421throw new Error(`Method ${String(property)} not implemented`);422}423return result;424},425});426};427428export interface SetWorkspaceTimeoutResult {429resetTimeoutOnWorkspaces: string[];430humanReadableDuration: string;431}432433export interface GetWorkspaceTimeoutResult {434duration: WorkspaceTimeoutDuration;435canChange: boolean;436humanReadableDuration: string;437}438439export interface StartWorkspaceResult {440instanceID: string;441workspaceURL?: string;442}443444export namespace GitpodServer {445export interface GetWorkspacesOptions {446limit?: number;447searchString?: string;448pinnedOnly?: boolean;449projectId?: string | string[];450includeWithoutProject?: boolean;451organizationId?: string;452}453export interface GetAccountStatementOptions {454date?: string;455}456export interface CreateWorkspaceOptions extends StartWorkspaceOptions {457contextUrl: string;458organizationId: string;459projectId?: string;460461// whether running workspaces on the same context should be ignored. If false (default) users will be asked.462//TODO(se) remove this option and let clients do that check if they like. The new create workspace page does it already463ignoreRunningWorkspaceOnSameCommit?: boolean;464forceDefaultConfig?: boolean;465}466467export interface StartWorkspaceOptions {468//TODO(cw): none of these options can be changed for a workspace that's been created. Should be moved to CreateWorkspaceOptions.469forceDefaultImage?: boolean;470workspaceClass?: string;471ideSettings?: IDESettings;472region?: WorkspaceRegion;473}474export interface TakeSnapshotOptions {475workspaceId: string;476/* this is here to enable backwards-compatibility and untangling rollout between workspace, IDE and meta */477dontWait?: boolean;478}479export interface GetTokenSearchOptions {480readonly host: string;481}482export interface SendHeartBeatOptions {483readonly instanceId: string;484readonly wasClosed?: boolean;485readonly roundTripTime?: number;486}487export interface UpdateOwnAuthProviderParams {488readonly entry: AuthProviderEntry.UpdateEntry | AuthProviderEntry.NewEntry;489}490export interface DeleteOwnAuthProviderParams {491readonly id: string;492}493export interface CreateOrgAuthProviderParams {494// ownerId is automatically set to the authenticated user495readonly entry: Omit<AuthProviderEntry.NewOrgEntry, "ownerId">;496}497export interface UpdateOrgAuthProviderParams {498readonly entry: AuthProviderEntry.UpdateOrgEntry;499}500export interface GetOrgAuthProviderParams {501readonly organizationId: string;502}503export interface DeleteOrgAuthProviderParams {504readonly id: string;505readonly organizationId: string;506}507export type AdmissionLevel = "owner" | "everyone";508export type PinAction = "pin" | "unpin" | "toggle";509export interface GenerateNewGitpodTokenOptions {510name?: string;511type: GitpodTokenType;512scopes?: string[];513}514export interface OnboardingState {515/**516* Whether this Gitpod instance is already configured with SSO.517*/518readonly isCompleted: boolean;519/**520* Total number of organizations.521*/522readonly organizationCountTotal: number;523}524}525526export const GitpodServerPath = "/gitpod";527528export const GitpodServerProxy = Symbol("GitpodServerProxy");529export type GitpodServerProxy<S extends GitpodServer> = JsonRpcProxy<S>;530531export class GitpodCompositeClient<Client extends GitpodClient> implements GitpodClient {532protected clients: Partial<Client>[] = [];533534public registerClient(client: Partial<Client>): Disposable {535this.clients.push(client);536return {537dispose: () => {538const index = this.clients.indexOf(client);539if (index > -1) {540this.clients.splice(index, 1);541}542},543};544}545546onInstanceUpdate(instance: WorkspaceInstance): void {547for (const client of this.clients) {548if (client.onInstanceUpdate) {549try {550client.onInstanceUpdate(instance);551} catch (error) {552console.error(error);553}554}555}556}557558onPrebuildUpdate(update: PrebuildWithStatus): void {559for (const client of this.clients) {560if (client.onPrebuildUpdate) {561try {562client.onPrebuildUpdate(update);563} catch (error) {564console.error(error);565}566}567}568}569570onWorkspaceImageBuildLogs(571info: WorkspaceImageBuild.StateInfo,572content: WorkspaceImageBuild.LogContent | undefined,573): void {574for (const client of this.clients) {575if (client.onWorkspaceImageBuildLogs) {576try {577client.onWorkspaceImageBuildLogs(info, content);578} catch (error) {579console.error(error);580}581}582}583}584585notifyDidOpenConnection(): void {586for (const client of this.clients) {587if (client.notifyDidOpenConnection) {588try {589client.notifyDidOpenConnection();590} catch (error) {591console.error(error);592}593}594}595}596597notifyDidCloseConnection(): void {598for (const client of this.clients) {599if (client.notifyDidCloseConnection) {600try {601client.notifyDidCloseConnection();602} catch (error) {603console.error(error);604}605}606}607}608}609610export type GitpodService = GitpodServiceImpl<GitpodClient, GitpodServer>;611612const hasWindow = typeof window !== "undefined";613const phasesOrder: Record<WorkspaceInstancePhase, number> = {614unknown: 0,615preparing: 1,616building: 2,617pending: 3,618creating: 4,619initializing: 5,620running: 6,621interrupted: 7,622stopping: 8,623stopped: 9,624};625export class WorkspaceInstanceUpdateListener {626private readonly onDidChangeEmitter = new Emitter<void>();627readonly onDidChange = this.onDidChangeEmitter.event;628629private source: "sync" | "update" = "sync";630631get info(): WorkspaceInfo {632return this._info;633}634635constructor(private readonly service: GitpodService, private _info: WorkspaceInfo) {636service.registerClient({637onInstanceUpdate: (instance) => {638if (this.isOutOfOrder(instance)) {639return;640}641this.cancelSync();642this._info.latestInstance = instance;643this.source = "update";644this.onDidChangeEmitter.fire(undefined);645},646notifyDidOpenConnection: () => {647this.sync();648},649});650if (hasWindow) {651// learn about page lifecycle here: https://developers.google.com/web/updates/2018/07/page-lifecycle-api652window.document.addEventListener("visibilitychange", async () => {653if (window.document.visibilityState === "visible") {654this.sync();655}656});657window.addEventListener("pageshow", (e) => {658if (e.persisted) {659this.sync();660}661});662}663}664665private syncQueue = Promise.resolve();666private syncTokenSource: CancellationTokenSource | undefined;667/**668* Only one sync can be performed at the same time.669* Any new sync request or instance update cancels all previously scheduled sync requests.670*/671private sync(): void {672this.cancelSync();673this.syncTokenSource = new CancellationTokenSource();674const token = this.syncTokenSource.token;675this.syncQueue = this.syncQueue.then(async () => {676if (token.isCancellationRequested) {677return;678}679try {680const info = await this.service.server.getWorkspace(this._info.workspace.id);681if (token.isCancellationRequested) {682return;683}684this._info = info;685this.source = "sync";686this.onDidChangeEmitter.fire(undefined);687} catch (e) {688console.error("failed to sync workspace instance:", e);689}690});691}692private cancelSync(): void {693if (this.syncTokenSource) {694this.syncTokenSource.cancel();695this.syncTokenSource = undefined;696}697}698699/**700* If sync seen more recent update then ignore all updates with previous phases.701* Within the same phase still the race can occur but which should be eventually consistent.702*/703private isOutOfOrder(instance: WorkspaceInstance): boolean {704if (instance.workspaceId !== this._info.workspace.id) {705return true;706}707if (this.source === "update") {708return false;709}710if (instance.id !== this.info.latestInstance?.id) {711return false;712}713return phasesOrder[instance.status.phase] < phasesOrder[this.info.latestInstance.status.phase];714}715}716717export interface GitpodServiceOptions {718onReconnect?: () => void | Promise<void>;719}720721export class GitpodServiceImpl<Client extends GitpodClient, Server extends GitpodServer> {722private readonly compositeClient = new GitpodCompositeClient<Client>();723724constructor(public readonly server: JsonRpcProxy<Server>, private options?: GitpodServiceOptions) {725server.setClient(this.compositeClient);726server.onDidOpenConnection(() => this.compositeClient.notifyDidOpenConnection());727server.onDidCloseConnection(() => this.compositeClient.notifyDidCloseConnection());728}729730public registerClient(client: Partial<Client>): Disposable {731return this.compositeClient.registerClient(client);732}733734private readonly instanceListeners = new Map<string, Promise<WorkspaceInstanceUpdateListener>>();735listenToInstance(workspaceId: string): Promise<WorkspaceInstanceUpdateListener> {736const listener =737this.instanceListeners.get(workspaceId) ||738(async () => {739const info = await this.server.getWorkspace(workspaceId);740return new WorkspaceInstanceUpdateListener(this, info);741})();742this.instanceListeners.set(workspaceId, listener);743return listener;744}745746async reconnect(): Promise<void> {747if (this.options?.onReconnect) {748await this.options.onReconnect();749}750}751}752753754