broadcast profiles changes to other windows (#154782)

* broadcast profiles changes to other windows

* - add type to broadcast data
- adopt in storage service
This commit is contained in:
Sandeep Somavarapu authored and GitHub committed 2022-07-11 17:53:08 +02:00
1 parent e02c71e3c6
commit ca5430942d
5 files changed
+109 -94

No files matched your search

+69
View File
@@ -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<T> extends Disposable {
private broadcastChannel: BroadcastChannel | undefined;
private readonly _onDidReceiveData = this._register(new Emitter<T>());
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));
}
}
}
@@ -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 IFileChange[]>());
readonly onDidFileChanges: Event<readonly IFileChange[]> = 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<UriDto<IFileChange>[]> | undefined;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = 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<UriDto<IFileChange>[]>(`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);
}
}
@@ -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<Omit<DidChangeProfilesEvent, 'all'>>;
export class BrowserUserDataProfilesService extends UserDataProfilesService implements IUserDataProfilesService {
protected override readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = true;
private readonly changesBroadcastChannel: BroadcastDataChannel<BroadcastedProfileChanges>;
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<BroadcastedProfileChanges>(`${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));
}
@@ -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 });
}
@@ -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<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
private broadcastChannel: BroadcastChannel | undefined;
private broadcastChannel: BroadcastDataChannel<IStorageItemsChangeEvent> | undefined;
private pendingUpdate: Promise<boolean> | 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<IStorageItemsChangeEvent>(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);
}
}