web - first cut user data provider storage service

This commit is contained in:
Benjamin Pasero 2019-07-05 16:26:32 +02:00
parent c212dda010
commit a65e9ca420
14 changed files with 650 additions and 543 deletions

View file

@ -0,0 +1,318 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isUndefinedOrNull } from 'vs/base/common/types';
export enum StorageHint {
// A hint to the storage that the storage
// does not exist on disk yet. This allows
// the storage library to improve startup
// time by not checking the storage for data.
STORAGE_DOES_NOT_EXIST
}
export interface IStorageOptions {
hint?: StorageHint;
}
export interface IUpdateRequest {
insert?: Map<string, string>;
delete?: Set<string>;
}
export interface IStorageItemsChangeEvent {
items: Map<string, string>;
}
export interface IStorageDatabase {
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
getItems(): Promise<Map<string, string>>;
updateItems(request: IUpdateRequest): Promise<void>;
close(recovery?: () => Map<string, string>): Promise<void>;
}
export interface IStorage extends IDisposable {
readonly items: Map<string, string>;
readonly size: number;
readonly onDidChangeStorage: Event<string>;
init(): Promise<void>;
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
delete(key: string): Promise<void>;
close(): Promise<void>;
}
enum StorageState {
None,
Initialized,
Closed
}
export class Storage extends Disposable implements IStorage {
private static readonly DEFAULT_FLUSH_DELAY = 100;
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
private state = StorageState.None;
private cache: Map<string, string> = new Map<string, string>();
private flushDelayer: ThrottledDelayer<void>;
private pendingDeletes: Set<string> = new Set<string>();
private pendingInserts: Map<string, string> = new Map();
constructor(
protected database: IStorageDatabase,
private options: IStorageOptions = Object.create(null)
) {
super();
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
this.registerListeners();
}
private registerListeners(): void {
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
}
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
// items that change external require us to update our
// caches with the values. we just accept the value and
// emit an event if there is a change.
e.items.forEach((value, key) => this.accept(key, value));
}
private accept(key: string, value: string): void {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
let changed = false;
// Item got removed, check for deletion
if (isUndefinedOrNull(value)) {
changed = this.cache.delete(key);
}
// Item got updated, check for change
else {
const currentValue = this.cache.get(key);
if (currentValue !== value) {
this.cache.set(key, value);
changed = true;
}
}
// Signal to outside listeners
if (changed) {
this._onDidChangeStorage.fire(key);
}
}
get items(): Map<string, string> {
return this.cache;
}
get size(): number {
return this.cache.size;
}
async init(): Promise<void> {
if (this.state !== StorageState.None) {
return Promise.resolve(); // either closed or already initialized
}
this.state = StorageState.Initialized;
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
// return early if we know the storage file does not exist. this is a performance
// optimization to not load all items of the underlying storage if we know that
// there can be no items because the storage does not exist.
return Promise.resolve();
}
this.cache = await this.database.getItems();
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
const value = this.cache.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
getNumber(key: string, fallbackValue?: number): number | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.delete(key);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.cache.get(key);
if (currentValue === valueStr) {
return Promise.resolve();
}
// Update in cache and pending
this.cache.set(key, valueStr);
this.pendingInserts.set(key, valueStr);
this.pendingDeletes.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
delete(key: string): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// Remove from cache and add to pending
const wasDeleted = this.cache.delete(key);
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
}
if (!this.pendingDeletes.has(key)) {
this.pendingDeletes.add(key);
}
this.pendingInserts.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
async close(): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // return if already closed
}
// Update state
this.state = StorageState.Closed;
// Trigger new flush to ensure data is persisted and then close
// even if there is an error flushing. We must always ensure
// the DB is closed to avoid corruption.
//
// Recovery: we pass our cache over as recovery option in case
// the DB is not healthy.
try {
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
} catch (error) {
// Ignore
}
await this.database.close(() => this.cache);
}
private flushPending(): Promise<void> {
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
return Promise.resolve(); // return early if nothing to do
}
// Get pending data
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
// Reset pending data for next run
this.pendingDeletes = new Set<string>();
this.pendingInserts = new Map<string, string>();
// Update in storage
return this.database.updateItems(updateRequest);
}
}
export class InMemoryStorageDatabase implements IStorageDatabase {
readonly onDidChangeItemsExternal = Event.None;
private items = new Map<string, string>();
getItems(): Promise<Map<string, string>> {
return Promise.resolve(this.items);
}
updateItems(request: IUpdateRequest): Promise<void> {
if (request.insert) {
request.insert.forEach((value, key) => this.items.set(key, value));
}
if (request.delete) {
request.delete.forEach(key => this.items.delete(key));
}
return Promise.resolve();
}
close(): Promise<void> {
return Promise.resolve();
}
}

View file

@ -4,305 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { Database, Statement } from 'vscode-sqlite3';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { ThrottledDelayer, timeout } from 'vs/base/common/async';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { Event } from 'vs/base/common/event';
import { timeout } from 'vs/base/common/async';
import { mapToString, setToString } from 'vs/base/common/map';
import { basename } from 'vs/base/common/path';
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
import { fill } from 'vs/base/common/arrays';
export enum StorageHint {
// A hint to the storage that the storage
// does not exist on disk yet. This allows
// the storage library to improve startup
// time by not checking the storage for data.
STORAGE_DOES_NOT_EXIST
}
export interface IStorageOptions {
hint?: StorageHint;
}
export interface IUpdateRequest {
insert?: Map<string, string>;
delete?: Set<string>;
}
export interface IStorageItemsChangeEvent {
items: Map<string, string>;
}
export interface IStorageDatabase {
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
getItems(): Promise<Map<string, string>>;
updateItems(request: IUpdateRequest): Promise<void>;
close(recovery?: () => Map<string, string>): Promise<void>;
checkIntegrity(full: boolean): Promise<string>;
}
export interface IStorage extends IDisposable {
readonly items: Map<string, string>;
readonly size: number;
readonly onDidChangeStorage: Event<string>;
init(): Promise<void>;
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
delete(key: string): Promise<void>;
close(): Promise<void>;
checkIntegrity(full: boolean): Promise<string>;
}
enum StorageState {
None,
Initialized,
Closed
}
export class Storage extends Disposable implements IStorage {
_serviceBrand: any;
private static readonly DEFAULT_FLUSH_DELAY = 100;
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
private state = StorageState.None;
private cache: Map<string, string> = new Map<string, string>();
private flushDelayer: ThrottledDelayer<void>;
private pendingDeletes: Set<string> = new Set<string>();
private pendingInserts: Map<string, string> = new Map();
constructor(
protected database: IStorageDatabase,
private options: IStorageOptions = Object.create(null)
) {
super();
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
this.registerListeners();
}
private registerListeners(): void {
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
}
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
// items that change external require us to update our
// caches with the values. we just accept the value and
// emit an event if there is a change.
e.items.forEach((value, key) => this.accept(key, value));
}
private accept(key: string, value: string): void {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
let changed = false;
// Item got removed, check for deletion
if (isUndefinedOrNull(value)) {
changed = this.cache.delete(key);
}
// Item got updated, check for change
else {
const currentValue = this.cache.get(key);
if (currentValue !== value) {
this.cache.set(key, value);
changed = true;
}
}
// Signal to outside listeners
if (changed) {
this._onDidChangeStorage.fire(key);
}
}
get items(): Map<string, string> {
return this.cache;
}
get size(): number {
return this.cache.size;
}
async init(): Promise<void> {
if (this.state !== StorageState.None) {
return Promise.resolve(); // either closed or already initialized
}
this.state = StorageState.Initialized;
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
// return early if we know the storage file does not exist. this is a performance
// optimization to not load all items of the underlying storage if we know that
// there can be no items because the storage does not exist.
return Promise.resolve();
}
this.cache = await this.database.getItems();
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
const value = this.cache.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
getNumber(key: string, fallbackValue?: number): number | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.delete(key);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.cache.get(key);
if (currentValue === valueStr) {
return Promise.resolve();
}
// Update in cache and pending
this.cache.set(key, valueStr);
this.pendingInserts.set(key, valueStr);
this.pendingDeletes.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
delete(key: string): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// Remove from cache and add to pending
const wasDeleted = this.cache.delete(key);
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
}
if (!this.pendingDeletes.has(key)) {
this.pendingDeletes.add(key);
}
this.pendingInserts.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
async close(): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // return if already closed
}
// Update state
this.state = StorageState.Closed;
// Trigger new flush to ensure data is persisted and then close
// even if there is an error flushing. We must always ensure
// the DB is closed to avoid corruption.
//
// Recovery: we pass our cache over as recovery option in case
// the DB is not healthy.
try {
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
} catch (error) {
// Ignore
}
await this.database.close(() => this.cache);
}
private flushPending(): Promise<void> {
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
return Promise.resolve(); // return early if nothing to do
}
// Get pending data
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
// Reset pending data for next run
this.pendingDeletes = new Set<string>();
this.pendingInserts = new Map<string, string>();
// Update in storage
return this.database.updateItems(updateRequest);
}
checkIntegrity(full: boolean): Promise<string> {
return this.database.checkIntegrity(full);
}
}
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
interface IDatabaseConnection {
db: Database;
@ -753,34 +461,3 @@ class SQLiteStorageDatabaseLogger {
}
}
}
export class InMemoryStorageDatabase implements IStorageDatabase {
readonly onDidChangeItemsExternal = Event.None;
private items = new Map<string, string>();
getItems(): Promise<Map<string, string>> {
return Promise.resolve(this.items);
}
updateItems(request: IUpdateRequest): Promise<void> {
if (request.insert) {
request.insert.forEach((value, key) => this.items.set(key, value));
}
if (request.delete) {
request.delete.forEach(key => this.items.delete(key));
}
return Promise.resolve();
}
close(): Promise<void> {
return Promise.resolve();
}
checkIntegrity(full: boolean): Promise<string> {
return Promise.resolve('ok');
}
}

View file

@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Storage, SQLiteStorageDatabase, IStorageDatabase, ISQLiteStorageDatabaseOptions, IStorageItemsChangeEvent } from 'vs/base/node/storage';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';

View file

@ -67,7 +67,8 @@ suite('Multicursor selection', () => {
getBoolean: (key: string) => !!queryState[key],
getNumber: (key: string) => undefined!,
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
remove: (key) => undefined
remove: (key) => undefined,
logStorage: () => undefined
} as IStorageService);
test('issue #8817: Cursor position changes when you cancel multicursor', () => {

View file

@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files';
import { IStorage, IStorageDatabase, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath } from 'vs/base/common/resources';
export class BrowserStorageService extends Disposable implements IStorageService {
_serviceBrand: ServiceIdentifier<any>;
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
private readonly _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
private globalStorage: IStorage;
private workspaceStorage: IStorage;
private globalStorageFile: URI;
private workspaceStorageFile: URI;
private initializePromise: Promise<void>;
constructor(
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super();
}
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize(payload);
}
return this.initializePromise;
}
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
// Workspace Storage
this.workspaceStorageFile = joinPath(this.environmentService.userRoamingDataHome, 'workspaceStorage', `${payload.id}.json`);
this.workspaceStorage = new Storage(this._register(new FileStorageDatabase(this.workspaceStorageFile, this.fileService)));
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
// Global Storage
this.globalStorageFile = joinPath(this.environmentService.userRoamingDataHome, 'globalStorage', 'global.json');
this.globalStorage = new Storage(this._register(new FileStorageDatabase(this.globalStorageFile, this.fileService)));
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
// Init both
await Promise.all([
this.workspaceStorage.init(),
this.globalStorage.init()
]);
}
//#region
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope).get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope).getBoolean(key, fallbackValue);
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope): number | undefined;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope).getNumber(key, fallbackValue);
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
this.getStorage(scope).set(key, value);
}
remove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
async close(): Promise<void> {
// Signal as event so that clients can still store data
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
// Do it
await Promise.all([
this.globalStorage.close(),
this.workspaceStorage.close()
]);
}
private getStorage(scope: StorageScope): IStorage {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
async logStorage(): Promise<void> {
const result = await Promise.all([
this.globalStorage.items,
this.workspaceStorage.items
]);
return logStorage(result[0], result[1], this.globalStorageFile.toString(), this.workspaceStorageFile.toString());
}
//#endregion
}
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
readonly onDidChangeItemsExternal = Event.None; // TODO@Ben implement global UI storage events
private cache: Map<string, string> | undefined;
private pendingUpdate: Promise<void> = Promise.resolve();
constructor(
private readonly file: URI,
private readonly fileService: IFileService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.fileService.watch(this.file));
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
}
private onFileChanges(e: FileChangesEvent): void {
}
async getItems(): Promise<Map<string, string>> {
if (!this.cache) {
try {
this.cache = await this.doGetItemsFromFile();
} catch (error) {
this.cache = new Map();
}
}
return this.cache;
}
private async doGetItemsFromFile(): Promise<Map<string, string>> {
await this.pendingUpdate;
const itemsRaw = await this.fileService.readFile(this.file);
return new Map(JSON.parse(itemsRaw.value.toString()));
}
async updateItems(request: IUpdateRequest): Promise<void> {
let updateCount = 0;
if (request.insert) {
updateCount += request.insert.size;
}
if (request.delete) {
updateCount += request.delete.size;
}
if (updateCount === 0) {
return Promise.resolve();
}
const items = await this.getItems();
if (request.insert) {
request.insert.forEach((value, key) => items.set(key, value));
}
if (request.delete) {
request.delete.forEach(key => items.delete(key));
}
const itemsRaw = JSON.stringify([...items]);
await this.pendingUpdate;
this.pendingUpdate = this.fileService.writeFile(this.file, VSBuffer.fromString(itemsRaw)).then();
return this.pendingUpdate;
}
close(): Promise<void> {
return this.pendingUpdate;
}
}

View file

@ -86,6 +86,11 @@ export interface IStorageService {
* operation to either the current workspace only or all workspaces.
*/
remove(key: string, scope: StorageScope): void;
/**
* Log the contents of the storage to the console.
*/
logStorage(): void;
}
export const enum StorageScope {
@ -107,6 +112,7 @@ export interface IWorkspaceStorageChangeEvent {
}
export class InMemoryStorageService extends Disposable implements IStorageService {
_serviceBrand = undefined;
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
@ -190,4 +196,52 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
return Promise.resolve();
}
logStorage(): void {
logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory');
}
}
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {
const safeParse = (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
const globalItems = new Map<string, string>();
const globalItemsParsed = new Map<string, string>();
global.forEach((value, key) => {
globalItems.set(key, value);
globalItemsParsed.set(key, safeParse(value));
});
const workspaceItems = new Map<string, string>();
const workspaceItemsParsed = new Map<string, string>();
workspace.forEach((value, key) => {
workspaceItems.set(key, value);
workspaceItemsParsed.set(key, safeParse(value));
});
console.group(`Storage: Global (path: ${globalPath})`);
let globalValues: { key: string, value: string }[] = [];
globalItems.forEach((value, key) => {
globalValues.push({ key, value });
});
console.table(globalValues);
console.groupEnd();
console.log(globalItemsParsed);
console.group(`Storage: Workspace (path: ${workspacePath})`);
let workspaceValues: { key: string, value: string }[] = [];
workspaceItems.forEach((value, key) => {
workspaceValues.push({ key, value });
});
console.table(workspaceValues);
console.groupEnd();
console.log(workspaceItemsParsed);
}

View file

@ -6,7 +6,7 @@
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { StorageMainService, IStorageChangeEvent } from 'vs/platform/storage/node/storageMainService';
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/node/storage';
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { onUnexpectedError } from 'vs/base/common/errors';
@ -139,10 +139,6 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
break;
}
case 'checkIntegrity': {
return this.storageMainService.checkIntegrity(arg);
}
default:
throw new Error(`Call not found: ${command}`);
}
@ -201,10 +197,6 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
return this.channel.call('updateItems', serializableRequest);
}
checkIntegrity(full: boolean): Promise<string> {
return this.channel.call('checkIntegrity', full);
}
close(): Promise<void> {
// when we are about to close, we start to ignore main-side changes since we close anyway

View file

@ -8,7 +8,8 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IStorage, Storage, SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions, InMemoryStorageDatabase } from 'vs/base/node/storage';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
@ -164,8 +165,4 @@ export class StorageMainService extends Disposable implements IStorageMainServic
// Do it
return this.storage.close();
}
checkIntegrity(full: boolean): Promise<string> {
return this.storage.checkIntegrity(full);
}
}

View file

@ -6,20 +6,20 @@
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { Storage, ISQLiteStorageDatabaseLoggingOptions, IStorage, StorageHint, IStorageDatabase, SQLiteStorageDatabase } from 'vs/base/node/storage';
import { Action } from 'vs/base/common/actions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { localize } from 'vs/nls';
import { mark, getDuration } from 'vs/base/common/performance';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
import { mark } from 'vs/base/common/performance';
import { join } from 'vs/base/common/path';
import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
export class StorageService extends Disposable implements IStorageService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<any>;
private static WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static WORKSPACE_META_NAME = 'workspace.json';
@ -201,63 +201,13 @@ export class StorageService extends Disposable implements IStorageService {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
getSize(scope: StorageScope): number {
return scope === StorageScope.GLOBAL ? this.globalStorage.size : this.workspaceStorage.size;
}
checkIntegrity(scope: StorageScope, full: boolean): Promise<string> {
return scope === StorageScope.GLOBAL ? this.globalStorage.checkIntegrity(full) : this.workspaceStorage.checkIntegrity(full);
}
async logStorage(): Promise<void> {
const result = await Promise.all([
this.globalStorage.items,
this.workspaceStorage.items,
this.globalStorage.checkIntegrity(true /* full */),
this.workspaceStorage.checkIntegrity(true /* full */)
this.workspaceStorage.items
]);
const safeParse = (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
const globalItems = new Map<string, string>();
const globalItemsParsed = new Map<string, string>();
result[0].forEach((value, key) => {
globalItems.set(key, value);
globalItemsParsed.set(key, safeParse(value));
});
const workspaceItems = new Map<string, string>();
const workspaceItemsParsed = new Map<string, string>();
result[1].forEach((value, key) => {
workspaceItems.set(key, value);
workspaceItemsParsed.set(key, safeParse(value));
});
console.group(`Storage: Global (integrity: ${result[2]}, path: ${this.environmentService.globalStorageHome})`);
let globalValues: { key: string, value: string }[] = [];
globalItems.forEach((value, key) => {
globalValues.push({ key, value });
});
console.table(globalValues);
console.groupEnd();
console.log(globalItemsParsed);
console.group(`Storage: Workspace (integrity: ${result[3]}, load: ${getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage')}, path: ${this.workspaceStoragePath})`);
let workspaceValues: { key: string, value: string }[] = [];
workspaceItems.forEach((value, key) => {
workspaceValues.push({ key, value });
});
console.table(workspaceValues);
console.groupEnd();
console.log(workspaceItemsParsed);
logStorage(result[0], result[1], this.environmentService.globalStorageHome, this.workspaceStoragePath);
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
@ -280,24 +230,3 @@ export class StorageService extends Disposable implements IStorageService {
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
}
}
export class LogStorageAction extends Action {
static readonly ID = 'workbench.action.logStorage';
static LABEL = localize({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents");
constructor(
id: string,
label: string,
@IStorageService private readonly storageService: StorageService,
@IWindowService private readonly windowService: IWindowService
) {
super(id, label);
}
run(): Promise<void> {
this.storageService.logStorage();
return this.windowService.openDevTools();
}
}

View file

@ -13,7 +13,7 @@ import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/node/storage';
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
suite('StorageService', () => {

View file

@ -19,6 +19,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class InspectContextKeysAction extends Action {
@ -193,7 +194,29 @@ export class ToggleScreencastModeAction extends Action {
}
}
export class LogStorageAction extends Action {
static readonly ID = 'workbench.action.logStorage';
static LABEL = nls.localize({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents");
constructor(
id: string,
label: string,
@IStorageService private readonly storageService: IStorageService,
@IWindowService private readonly windowService: IWindowService
) {
super(id, label);
}
run(): Promise<void> {
this.storageService.logStorage();
return this.windowService.openDevTools();
}
}
const developerCategory = nls.localize('developer', "Developer");
const registry = Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(InspectContextKeysAction, InspectContextKeysAction.ID, InspectContextKeysAction.LABEL), 'Developer: Inspect Context Keys', developerCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleScreencastModeAction, ToggleScreencastModeAction.ID, ToggleScreencastModeAction.LABEL), 'Developer: Toggle Screencast Mode', developerCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(LogStorageAction, LogStorageAction.ID, LogStorageAction.LABEL), 'Developer: Log Storage Database Contents', developerCategory);

View file

@ -35,8 +35,10 @@ import { hash } from 'vs/base/common/hash';
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
import { ProductService } from 'vs/platform/product/browser/productService';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { joinPath } from 'vs/base/common/resources';
import { BACKUPS } from 'vs/platform/environment/common/environment';
import { joinPath } from 'vs/base/common/resources';
import { BrowserStorageService } from 'vs/platform/storage/browser/storageService';
import { IStorageService } from 'vs/platform/storage/common/storage';
class CodeRendererMain extends Disposable {
@ -146,11 +148,34 @@ class CodeRendererMain extends Disposable {
return service;
}),
this.createStorageService(payload, environmentService, fileService, logService).then(service => {
// Storage
serviceCollection.set(IStorageService, service);
return service;
})
]);
return { serviceCollection, logService };
}
private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise<IStorageService> {
const storageService = new BrowserStorageService(environmentService, fileService);
try {
await storageService.initialize(payload);
return storageService;
} catch (error) {
onUnexpectedError(error);
logService.error(error);
return storageService;
}
}
private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise<WorkspaceService> {
const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService);

View file

@ -6,7 +6,7 @@
import { URI } from 'vs/base/common/uri';
import * as browser from 'vs/base/browser/browser';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
// tslint:disable-next-line: import-patterns no-standalone-editor
@ -18,9 +18,8 @@ import { IExtensionManifest, ExtensionType, ExtensionIdentifier, IExtension } fr
import { IURLHandler, IURLService } from 'vs/platform/url/common/url';
import { ITelemetryService, ITelemetryData, ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry';
import { ConsoleLogService } from 'vs/platform/log/common/log';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IUpdateService, State } from 'vs/platform/update/common/update';
import { IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService, IOpenSettings, IWindowSettings } from 'vs/platform/windows/common/windows';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
@ -39,7 +38,6 @@ import { ICommentService, IResourceCommentThreadEvent, IWorkspaceCommentThreadsE
import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { CommentingRanges } from 'vs/editor/common/modes';
import { Range } from 'vs/editor/common/core/range';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService';
@ -86,14 +84,6 @@ registerSingleton(IClipboardService, SimpleClipboardService, true);
//#endregion
//#region Dialog
// export class SimpleDialogService extends StandaloneEditorDialogService { }
// registerSingleton(IDialogService, SimpleDialogService, true);
//#endregion
//#region Download
export class SimpleDownloadService implements IDownloadService {
@ -482,111 +472,6 @@ export class SimpleRequestService implements IRequestService {
//#endregion
//#region Storage
export class LocalStorageService extends Disposable implements IStorageService {
_serviceBrand = undefined;
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
private readonly _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
constructor(
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
@ILifecycleService lifecycleService: ILifecycleService
) {
super();
this._register(lifecycleService.onBeforeShutdown(() => this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN })));
}
private toKey(key: string, scope: StorageScope): string {
if (scope === StorageScope.GLOBAL) {
return `global://${key}`;
}
return `workspace://${this.workspaceContextService.getWorkspace().id}/${key}`;
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
const value = window.localStorage.getItem(this.toKey(key, scope));
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
const value = window.localStorage.getItem(this.toKey(key, scope));
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
const value = window.localStorage.getItem(this.toKey(key, scope));
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.remove(key, scope);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = window.localStorage.getItem(this.toKey(key, scope));
if (currentValue === valueStr) {
return Promise.resolve();
}
// Update in cache
window.localStorage.setItem(this.toKey(key, scope), valueStr);
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
}
remove(key: string, scope: StorageScope): Promise<void> {
const wasDeleted = window.localStorage.getItem(this.toKey(key, scope));
window.localStorage.removeItem(this.toKey(key, scope));
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
}
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
}
}
registerSingleton(IStorageService, LocalStorageService);
//#endregion
//#region Telemetry
export class SimpleTelemetryService implements ITelemetryService {

View file

@ -23,7 +23,6 @@ import { ADD_ROOT_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspa
import { SupportsWorkspacesContext, IsMacContext, HasMacNativeTabsContext, IsDevelopmentContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor';
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
import { LogStorageAction } from 'vs/platform/storage/node/storageService';
import product from 'vs/platform/product/node/product';
// Actions
@ -125,7 +124,6 @@ import product from 'vs/platform/product/node/product';
const developerCategory = nls.localize('developer', "Developer");
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSharedProcessAction, ToggleSharedProcessAction.ID, ToggleSharedProcessAction.LABEL), 'Developer: Toggle Shared Process', developerCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(ReloadWindowWithExtensionsDisabledAction, ReloadWindowWithExtensionsDisabledAction.ID, ReloadWindowWithExtensionsDisabledAction.LABEL), 'Developer: Reload Window With Extensions Disabled', developerCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(LogStorageAction, LogStorageAction.ID, LogStorageAction.LABEL), 'Developer: Log Storage Database Contents', developerCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleDevToolsAction, ToggleDevToolsAction.ID, ToggleDevToolsAction.LABEL), 'Developer: Toggle Developer Tools', developerCategory);
KeybindingsRegistry.registerKeybindingRule({