From ca5430942daf044b8f40545f6811d21173a84591 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 11 Jul 2022 17:53:08 +0200 Subject: [PATCH] broadcast profiles changes to other windows (#154782) * broadcast profiles changes to other windows * - add type to broadcast data - adopt in storage service --- src/vs/base/browser/broadcast.ts | 69 ++++++++++++++ .../browser/indexedDBFileSystemProvider.ts | 90 +++---------------- .../browser/userDataProfile.ts | 19 +++- .../userDataProfile/common/userDataProfile.ts | 4 + .../storage/browser/storageService.ts | 21 ++--- 5 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 src/vs/base/browser/broadcast.ts diff --git a/src/vs/base/browser/broadcast.ts b/src/vs/base/browser/broadcast.ts new file mode 100644 index 000000000000..d7785f42ff81 --- /dev/null +++ b/src/vs/base/browser/broadcast.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getErrorMessage } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; + +export class BroadcastDataChannel extends Disposable { + + private broadcastChannel: BroadcastChannel | undefined; + + private readonly _onDidReceiveData = this._register(new Emitter()); + readonly onDidReceiveData = this._onDidReceiveData.event; + + constructor(private readonly channelName: string) { + super(); + + // Use BroadcastChannel + if ('BroadcastChannel' in window) { + try { + this.broadcastChannel = new BroadcastChannel(channelName); + const listener = (event: MessageEvent) => { + this._onDidReceiveData.fire(event.data); + }; + this.broadcastChannel.addEventListener('message', listener); + this._register(toDisposable(() => { + if (this.broadcastChannel) { + this.broadcastChannel.removeEventListener('message', listener); + this.broadcastChannel.close(); + } + })); + } catch (error) { + console.warn('Error while creating broadcast channel. Falling back to localStorage.', getErrorMessage(error)); + } + } + + // BroadcastChannel is not supported. Use storage. + if (!this.broadcastChannel) { + this.channelName = `BroadcastDataChannel.${channelName}`; + this.createBroadcastChannel(); + } + } + + private createBroadcastChannel(): void { + const listener = (event: StorageEvent) => { + if (event.key === this.channelName && event.newValue) { + this._onDidReceiveData.fire(JSON.parse(event.newValue)); + } + }; + window.addEventListener('storage', listener); + this._register(toDisposable(() => window.removeEventListener('storage', listener))); + } + + /** + * Sends the data to other BroadcastChannel objects set up for this channel. Data can be structured objects, e.g. nested objects and arrays. + * @param data data to broadcast + */ + postData(data: T): void { + if (this.broadcastChannel) { + this.broadcastChannel.postMessage(data); + } else { + // remove previous changes so that event is triggered even if new changes are same as old changes + window.localStorage.removeItem(this.channelName); + window.localStorage.setItem(this.channelName, JSON.stringify(data)); + } + } +} diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index af4458fe1c0d..660dd5cdcc13 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -5,15 +5,15 @@ import { Throttler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; -import { getErrorMessage } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ExtUri } from 'vs/base/common/resources'; -import { isString } from 'vs/base/common/types'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { isString, UriDto } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; import { DBClosedError, IndexedDB } from 'vs/base/browser/indexedDB'; +import { BroadcastDataChannel } from 'vs/base/browser/broadcast'; export type IndexedDBFileSystemProviderErrorDataClassification = { owner: 'sandy081'; @@ -165,78 +165,6 @@ class IndexedDBFileSystemNode { } } -type FileChangeDto = { - readonly type: FileChangeType; - readonly resource: UriComponents; -}; - -class IndexedDBChangesBroadcastChannel extends Disposable { - - private broadcastChannel: BroadcastChannel | undefined; - - private readonly _onDidFileChanges = this._register(new Emitter()); - readonly onDidFileChanges: Event = this._onDidFileChanges.event; - - constructor(private readonly changesKey: string) { - super(); - - // Use BroadcastChannel - if ('BroadcastChannel' in window) { - try { - this.broadcastChannel = new BroadcastChannel(changesKey); - const listener = (event: MessageEvent) => { - if (isString(event.data)) { - this.onDidReceiveChanges(event.data); - } - }; - this.broadcastChannel.addEventListener('message', listener); - this._register(toDisposable(() => { - if (this.broadcastChannel) { - this.broadcastChannel.removeEventListener('message', listener); - this.broadcastChannel.close(); - } - })); - } catch (error) { - console.warn('Error while creating broadcast channel. Falling back to localStorage.', getErrorMessage(error)); - this.createStorageBroadcastChannel(changesKey); - } - } - - // BroadcastChannel is not supported. Use storage. - else { - this.createStorageBroadcastChannel(changesKey); - } - } - - private createStorageBroadcastChannel(changesKey: string): void { - const listener = (event: StorageEvent) => { - if (event.key === changesKey && event.newValue) { - this.onDidReceiveChanges(event.newValue); - } - }; - window.addEventListener('storage', listener); - this._register(toDisposable(() => window.removeEventListener('storage', listener))); - } - - private onDidReceiveChanges(data: string): void { - try { - const changesDto: FileChangeDto[] = JSON.parse(data); - this._onDidFileChanges.fire(changesDto.map(c => ({ type: c.type, resource: URI.revive(c.resource) }))); - } catch (error) {/* ignore*/ } - } - - postChanges(changes: IFileChange[]): void { - if (this.broadcastChannel) { - this.broadcastChannel.postMessage(JSON.stringify(changes)); - } else { - // remove previous changes so that event is triggered even if new changes are same as old changes - window.localStorage.removeItem(this.changesKey); - window.localStorage.setItem(this.changesKey, JSON.stringify(changes)); - } - } - -} - export class IndexedDBFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { readonly capabilities: FileSystemProviderCapabilities = @@ -246,7 +174,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst private readonly extUri = new ExtUri(() => false) /* Case Sensitive */; - private readonly changesBroadcastChannel: IndexedDBChangesBroadcastChannel | undefined; + private readonly changesBroadcastChannel: BroadcastDataChannel[]> | undefined; private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; @@ -263,8 +191,10 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst this.writeManyThrottler = new Throttler(); if (watchCrossWindowChanges) { - this.changesBroadcastChannel = this._register(new IndexedDBChangesBroadcastChannel(`vscode.indexedDB.${scheme}.changes`)); - this._register(this.changesBroadcastChannel.onDidFileChanges(changes => this._onDidChangeFile.fire(changes))); + this.changesBroadcastChannel = this._register(new BroadcastDataChannel[]>(`vscode.indexedDB.${scheme}.changes`)); + this._register(this.changesBroadcastChannel.onDidReceiveData(changes => { + this._onDidChangeFile.fire(changes.map(c => ({ type: c.type, resource: URI.revive(c.resource) }))); + })); } } @@ -459,7 +389,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst if (changes.length) { this._onDidChangeFile.fire(changes); - this.changesBroadcastChannel?.postChanges(changes); + this.changesBroadcastChannel?.postData(changes); } } diff --git a/src/vs/platform/userDataProfile/browser/userDataProfile.ts b/src/vs/platform/userDataProfile/browser/userDataProfile.ts index 56edfc09480d..b00f89a92b79 100644 --- a/src/vs/platform/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/browser/userDataProfile.ts @@ -3,16 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BroadcastDataChannel } from 'vs/base/browser/broadcast'; import { revive } from 'vs/base/common/marshalling'; +import { UriDto } from 'vs/base/common/types'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IUserDataProfilesService, PROFILES_ENABLEMENT_CONFIG, StoredProfileAssociations, StoredUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfilesService, PROFILES_ENABLEMENT_CONFIG, reviveProfile, StoredProfileAssociations, StoredUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; + +type BroadcastedProfileChanges = UriDto>; export class BrowserUserDataProfilesService extends UserDataProfilesService implements IUserDataProfilesService { protected override readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = true; + private readonly changesBroadcastChannel: BroadcastDataChannel; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -22,6 +27,13 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl ) { super(environmentService, fileService, uriIdentityService, logService); super.setEnablement(window.localStorage.getItem(PROFILES_ENABLEMENT_CONFIG) === 'true'); + this.changesBroadcastChannel = this._register(new BroadcastDataChannel(`${UserDataProfilesService.PROFILES_KEY}.changes`)); + this._register(this.changesBroadcastChannel.onDidReceiveData(changes => { + try { + this._profilesObject = undefined; + this._onDidChangeProfiles.fire({ added: changes.added.map(p => reviveProfile(p, this.profilesHome.scheme)), removed: changes.removed.map(p => reviveProfile(p, this.profilesHome.scheme)), all: this.profiles }); + } catch (error) {/* ignore */ } + })); } override setEnablement(enabled: boolean): void { @@ -42,6 +54,11 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl return []; } + protected override triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) { + super.triggerProfilesChanges(added, removed); + this.changesBroadcastChannel.postData({ added, removed }); + } + protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { window.localStorage.setItem(UserDataProfilesService.PROFILES_KEY, JSON.stringify(storedProfiles)); } diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 53ab77f58ec3..3bc2eb4ea3dd 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -338,6 +338,10 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } this.saveStoredProfiles(storedProfiles); this._profilesObject = undefined; + this.triggerProfilesChanges(added, removed); + } + + protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) { this._onDidChangeProfiles.fire({ added, removed, all: this.profiles }); } diff --git a/src/vs/workbench/services/storage/browser/storageService.ts b/src/vs/workbench/services/storage/browser/storageService.ts index e267855723c7..0266871f4989 100644 --- a/src/vs/workbench/services/storage/browser/storageService.ts +++ b/src/vs/workbench/services/storage/browser/storageService.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BroadcastDataChannel } from 'vs/base/browser/broadcast'; import { isSafari } from 'vs/base/browser/browser'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { DeferredPromise, Promises } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; import { InMemoryStorageDatabase, isStorageItemsChangeEvent, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; import { ILogService } from 'vs/platform/log/common/log'; @@ -302,7 +303,7 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt private readonly _onDidChangeItemsExternal = this._register(new Emitter()); readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; - private broadcastChannel: BroadcastChannel | undefined; + private broadcastChannel: BroadcastDataChannel | undefined; private pendingUpdate: Promise | undefined = undefined; get hasPendingUpdate(): boolean { return !!this.pendingUpdate; } @@ -317,7 +318,7 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt super(); this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${options.id}`; - this.broadcastChannel = options.broadcastChanges && ('BroadcastChannel' in window) ? new BroadcastChannel(IndexedDBStorageDatabase.STORAGE_BROADCAST_CHANNEL) : undefined; + this.broadcastChannel = options.broadcastChanges ? this._register(new BroadcastDataChannel(IndexedDBStorageDatabase.STORAGE_BROADCAST_CHANNEL)) : undefined; this.whenConnected = this.connect(); @@ -329,16 +330,10 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt // Check for storage change events from other // windows/tabs via `BroadcastChannel` mechanisms. if (this.broadcastChannel) { - const listener = (event: MessageEvent) => { - if (isStorageItemsChangeEvent(event.data)) { - this._onDidChangeItemsExternal.fire(event.data); + this._register(this.broadcastChannel.onDidReceiveData(data => { + if (isStorageItemsChangeEvent(data)) { + this._onDidChangeItemsExternal.fire(data); } - }; - - this.broadcastChannel.addEventListener('message', listener); - this._register(toDisposable(() => { - this.broadcastChannel?.removeEventListener('message', listener); - this.broadcastChannel?.close(); })); } } @@ -382,7 +377,7 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt deleted: request.delete }; - this.broadcastChannel.postMessage(event); + this.broadcastChannel.postData(event); } }