web - implemented indexedDB based storage service

This commit is contained in:
Benjamin Pasero 2021-06-06 08:27:13 +02:00
parent 4f3f431908
commit ad60f647d9
9 changed files with 361 additions and 183 deletions

View file

@ -220,6 +220,19 @@
"**/vs/platform/*/test/common/**"
]
},
{
"target": "**/vs/platform/*/test/browser/**",
"restrictions": [
"assert",
"sinon",
"vs/nls",
"**/vs/base/{common,browser}/**",
"**/vs/base/parts/*/{common,browser}/**",
"**/vs/base/test/{common,browser}/**",
"**/vs/platform/*/{common,browser}/**",
"**/vs/platform/*/test/{common,browser}/**"
]
},
{
"target": "**/vs/platform/*/browser/**",
"restrictions": [

View file

@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function flakySuite(title: string, fn: () => void) /* Suite */ {
return suite(title, function () {
// Flaky suites need retries and timeout to complete
// e.g. because they access browser features which can
// be unreliable depending on the environment.
this.retries(3);
this.timeout(1000 * 20);
// Invoke suite ensuring that `this` is
// properly wired in.
fn.call(this);
});
}

View file

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { Suite } from 'mocha';
import { join } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as testUtils from 'vs/base/test/common/testUtils';
export function getRandomTestPath(tmpdir: string, ...segments: string[]): string {
return join(tmpdir, ...segments, generateUuid());
@ -16,17 +16,4 @@ export function getPathFromAmdModule(requirefn: typeof require, relativePath: st
return URI.parse(requirefn.toUrl(relativePath)).fsPath;
}
export function flakySuite(title: string, fn: (this: Suite) => void): Suite {
return suite(title, function () {
// Flaky suites need retries and timeout to complete
// e.g. because they access the file system which can
// be unreliable depending on the environment.
this.retries(3);
this.timeout(1000 * 20);
// Invoke suite ensuring that `this` is
// properly wired in.
fn.call(this);
});
}
export import flakySuite = testUtils.flakySuite;

View file

@ -14,11 +14,9 @@ import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, I
import { assertIsDefined } from 'vs/base/common/types';
import { basename, joinPath } from 'vs/base/common/resources';
import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
import { flakySuite } from 'vs/base/test/common/testUtils';
suite('IndexedDB File Service', function () {
// IDB sometimes under pressure in build machines.
this.retries(3);
flakySuite('IndexedDB File Service', function () {
const logSchema = 'logs';

View file

@ -3,17 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IStorage, Storage, IStorageDatabase, IUpdateRequest, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { Promises } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { ILogService } from 'vs/platform/log/common/log';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { joinPath } from 'vs/base/common/resources';
export class BrowserStorageService extends AbstractStorageService {
@ -22,40 +22,41 @@ export class BrowserStorageService extends AbstractStorageService {
private globalStorage: IStorage | undefined;
private workspaceStorage: IStorage | undefined;
private globalStorageDatabase: FileStorageDatabase | undefined;
private workspaceStorageDatabase: FileStorageDatabase | undefined;
private globalStorageFile: URI | undefined;
private workspaceStorageFile: URI | undefined;
private globalStorageDatabase: IIndexedDBStorageDatabase | undefined;
private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined;
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
return Boolean(this.globalStorageDatabase?.hasPendingUpdate || this.workspaceStorageDatabase?.hasPendingUpdate);
}
constructor(
private readonly payload: IWorkspaceInitializationPayload,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL });
}
private getId(scope: StorageScope): string {
return scope === StorageScope.GLOBAL ? 'global' : this.payload.id;
}
protected async doInitialize(): Promise<void> {
// Ensure state folder exists
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
await this.fileService.createFolder(stateRoot);
// Create Storage in Parallel
const [workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled([
IndexedDBStorageDatabase.create(this.getId(StorageScope.WORKSPACE), this.logService),
IndexedDBStorageDatabase.create(this.getId(StorageScope.GLOBAL), this.logService)
]);
// Workspace Storage
this.workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
this.workspaceStorageDatabase = this._register(workspaceStorageDatabase);
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
// Global Storage
this.globalStorageFile = joinPath(stateRoot, 'global.json');
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
this.globalStorageDatabase = this._register(globalStorageDatabase);
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
@ -68,6 +69,7 @@ export class BrowserStorageService extends AbstractStorageService {
// Check to see if this is the first time we are "opening" the application
const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY);
if (firstOpen === undefined) {
await this.migrateOldStorage(StorageScope.GLOBAL); // TODO@bpasero remove browser storage migration
this.globalStorage.set(IS_NEW_KEY, true);
} else if (firstOpen) {
this.globalStorage.set(IS_NEW_KEY, false);
@ -76,18 +78,49 @@ export class BrowserStorageService extends AbstractStorageService {
// Check to see if this is the first time we are "opening" this workspace
const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY);
if (firstWorkspaceOpen === undefined) {
await this.migrateOldStorage(StorageScope.WORKSPACE); // TODO@bpasero remove browser storage migration
this.workspaceStorage.set(IS_NEW_KEY, true);
} else if (firstWorkspaceOpen) {
this.workspaceStorage.set(IS_NEW_KEY, false);
}
}
private async migrateOldStorage(scope: StorageScope): Promise<void> {
try {
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
if (scope === StorageScope.GLOBAL) {
const globalStorageFile = joinPath(stateRoot, 'global.json');
const globalItemsRaw = await this.fileService.readFile(globalStorageFile);
const globalItems = new Map<string, string>(JSON.parse(globalItemsRaw.value.toString()));
for (const [key, value] of globalItems) {
this.globalStorage?.set(key, value);
}
await this.fileService.del(globalStorageFile);
} else if (scope === StorageScope.WORKSPACE) {
const workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
const workspaceItemsRaw = await this.fileService.readFile(workspaceStorageFile);
const workspaceItems = new Map<string, string>(JSON.parse(workspaceItemsRaw.value.toString()));
for (const [key, value] of workspaceItems) {
this.workspaceStorage?.set(key, value);
}
await this.fileService.del(workspaceStorageFile);
}
} catch (error) {
// ignore
}
}
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString();
return this.getId(scope);
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
@ -118,144 +151,208 @@ export class BrowserStorageService extends AbstractStorageService {
// get triggered in this phase.
this.dispose();
}
async clear(): Promise<void> {
// Note: used for testing purposes only!
// Clear DBs
await Promises.settled([
this.globalStorageDatabase?.clear() ?? Promise.resolve(),
this.workspaceStorageDatabase?.clear() ?? Promise.resolve()
]);
// Flush to ensure data has been cleared
await this.flush();
}
}
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable {
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
/**
* Whether an update in the DB is currently pending
* (either update or delete operation).
*/
readonly hasPendingUpdate: boolean;
private cache: Map<string, string> | undefined;
/**
* For testing only.
*/
clear(): Promise<void>;
}
private pendingUpdate: Promise<void> = Promise.resolve();
class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implements IIndexedDBStorageDatabase {
private _hasPendingUpdate = false;
get hasPendingUpdate(): boolean {
return this._hasPendingUpdate;
readonly hasPendingUpdate = false;
async clear(): Promise<void> {
(await this.getItems()).clear();
}
private isWatching = false;
dispose(): void {
// No-op
}
}
constructor(
private readonly file: URI,
private readonly watchForExternalChanges: boolean,
@IFileService private readonly fileService: IFileService
export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase {
static async create(id: string, logService: ILogService): Promise<IIndexedDBStorageDatabase> {
try {
const database = new IndexedDBStorageDatabase(id, logService);
await database.whenConnected;
return database;
} catch (error) {
logService.error(`[IndexedDB Storage ${id}] create(): ${toErrorMessage(error, true)}`);
return new InMemoryIndexedDBStorageDatabase();
}
}
private static readonly STORAGE_DATABASE_PREFIX = 'vscode-web-state-db-';
private static readonly STORAGE_OBJECT_STORE = 'ItemTable';
readonly onDidChangeItemsExternal = Event.None; // IndexedDB currently does not support observers (https://github.com/w3c/IndexedDB/issues/51)
private pendingUpdate: Promise<void> | undefined = undefined;
get hasPendingUpdate(): boolean { return !!this.pendingUpdate; }
private readonly name: string;
private readonly whenConnected: Promise<IDBDatabase>;
private constructor(
id: string,
private readonly logService: ILogService
) {
super();
this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${id}`;
this.whenConnected = this.connect();
}
private async ensureWatching(): Promise<void> {
if (this.isWatching || !this.watchForExternalChanges) {
return;
}
private connect(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = window.indexedDB.open(this.name);
const exists = await this.fileService.exists(this.file);
if (this.isWatching || !exists) {
return; // file must exist to be watched
}
// Create `ItemTable` object-store when this DB is new
request.onupgradeneeded = () => {
request.result.createObjectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
};
this.isWatching = true;
// IndexedDB opened successfully
request.onsuccess = () => resolve(request.result);
this._register(this.fileService.watch(this.file));
this._register(this.fileService.onDidFilesChange(e => {
if (document.hasFocus()) {
return; // optimization: ignore changes from ourselves by checking for focus
}
if (!e.contains(this.file, FileChangeType.UPDATED)) {
return; // not our file
}
this.onDidStorageChangeExternal();
}));
// Fail on error (we will then fallback to in-memory DB)
request.onerror = () => reject(request.error);
});
}
private async onDidStorageChangeExternal(): Promise<void> {
const items = await this.doGetItemsFromFile();
getItems(): Promise<Map<string, string>> {
return new Promise<Map<string, string>>(async resolve => {
const items = new Map<string, string>();
// pervious cache, diff for changes
let changed = new Map<string, string>();
let deleted = new Set<string>();
if (this.cache) {
items.forEach((value, key) => {
const existingValue = this.cache?.get(key);
if (existingValue !== value) {
changed.set(key, value);
// Open a IndexedDB Cursor to iterate over key/values
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readonly');
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
const cursor = objectStore.openCursor();
if (!cursor) {
return resolve(items); // this means the `ItemTable` was empty
}
// Iterate over rows of `ItemTable` until the end
cursor.onsuccess = () => {
if (cursor.result) {
// Keep cursor key/value in our map
if (typeof cursor.result.value === 'string') {
items.set(cursor.result.key.toString(), cursor.result.value);
}
// Advance cursor to next row
cursor.result.continue();
} else {
resolve(items); // reached end of table
}
});
};
this.cache.forEach((_, key) => {
if (!items.has(key)) {
deleted.add(key);
}
});
}
const onError = (error: Error | null) => {
this.logService.error(`[IndexedDB Storage ${this.name}] getItems(): ${toErrorMessage(error, true)}`);
// no previous cache, consider all as changed
else {
changed = items;
}
resolve(items);
};
// Update cache
this.cache = items;
// Emit as event as needed
if (changed.size > 0 || deleted.size > 0) {
this._onDidChangeItemsExternal.fire({ changed, deleted });
}
}
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);
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
return new Map(JSON.parse(itemsRaw.value.toString()));
// Error handlers
cursor.onerror = () => onError(cursor.error);
transaction.onerror = () => onError(transaction.error);
});
}
async updateItems(request: IUpdateRequest): Promise<void> {
const items = await this.getItems();
this.pendingUpdate = this.doUpdateItems(request);
try {
await this.pendingUpdate;
} finally {
this.pendingUpdate = undefined;
}
}
if (request.insert) {
request.insert.forEach((value, key) => items.set(key, value));
private async doUpdateItems(request: IUpdateRequest): Promise<void> {
// Return early if the request is empty
const toInsert = request.insert;
const toDelete = request.delete;
if ((!toInsert && !toDelete) || (toInsert?.size === 0 && toDelete?.size === 0)) {
return;
}
if (request.delete) {
request.delete.forEach(key => items.delete(key));
}
// Update `ItemTable` with inserts and/or deletes
return new Promise<void>(async (resolve, reject) => {
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite');
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
// Inserts
if (toInsert) {
for (const [key, value] of toInsert) {
objectStore.put(value, key);
}
}
// Deletes
if (toDelete) {
for (const key of toDelete) {
objectStore.delete(key);
}
}
});
}
async close(): Promise<void> {
const db = await this.whenConnected;
// Wait for pending updates to having finished
await this.pendingUpdate;
this.pendingUpdate = (async () => {
try {
this._hasPendingUpdate = true;
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries()))));
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
} finally {
this._hasPendingUpdate = false;
}
})();
return this.pendingUpdate;
// Finally, close IndexedDB
return db.close();
}
close(): Promise<void> {
return this.pendingUpdate;
clear(): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite');
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
// Clear every row in the `ItemTable`
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
objectStore.clear();
});
}
}

View file

@ -4,18 +4,19 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual } from 'assert';
import { BrowserStorageService, FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { BrowserStorageService, IndexedDBStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { NullLogService } from 'vs/platform/log/common/log';
import { Storage } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { FileService } from 'vs/platform/files/common/fileService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { createSuite } from 'vs/platform/storage/test/common/storageService.test';
import { flakySuite } from 'vs/base/test/common/testUtils';
import { FileService } from 'vs/platform/files/common/fileService';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
suite('StorageService (browser)', function () {
flakySuite('StorageService (browser)', function () {
const disposables = new DisposableStore();
let storageService: BrowserStorageService;
@ -29,63 +30,81 @@ suite('StorageService (browser)', function () {
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
storageService = disposables.add(new BrowserStorageService({ id: String(Date.now()) }, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService));
storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, logService, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService));
await storageService.initialize();
return storageService;
},
teardown: async storage => {
await storageService.flush();
teardown: async () => {
await storageService.clear();
disposables.clear();
}
});
});
suite('FileStorageDatabase (browser)', () => {
flakySuite('IndexDBStorageDatabase (browser)', () => {
let fileService: FileService;
const id = 'workspace-storage-db-test';
const logService = new NullLogService();
const disposables = new DisposableStore();
setup(async () => {
const logService = new NullLogService();
fileService = disposables.add(new FileService(logService));
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
});
teardown(() => {
disposables.clear();
teardown(async () => {
const storage = await IndexedDBStorageDatabase.create(id, logService);
await storage.clear();
});
test('Basics', async () => {
const testDir = URI.file('/User/storage.json').with({ scheme: Schemas.userData });
let storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
// Insert initial data
storage.set('bar', 'foo');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
storage.set('barUndefined', undefined);
storage.set('barNull', null);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
strictEqual(storage.get('barUndefined'), undefined);
strictEqual(storage.get('barNull'), undefined);
await storage.close();
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
// Check initial data still there
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
strictEqual(storage.get('barUndefined'), undefined);
strictEqual(storage.get('barNull'), undefined);
// Update data
storage.set('bar', 'foo2');
storage.set('barNumber', 552);
strictEqual(storage.get('bar'), 'foo2');
strictEqual(storage.get('barNumber'), '552');
await storage.close();
storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
// Check initial data still there
strictEqual(storage.get('bar'), 'foo2');
strictEqual(storage.get('barNumber'), '552');
strictEqual(storage.get('barBoolean'), 'true');
strictEqual(storage.get('barUndefined'), undefined);
strictEqual(storage.get('barNull'), undefined);
// Delete data
storage.delete('bar');
storage.delete('barNumber');
storage.delete('barBoolean');
@ -96,7 +115,7 @@ suite('FileStorageDatabase (browser)', () => {
await storage.close();
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
@ -104,4 +123,37 @@ suite('FileStorageDatabase (browser)', () => {
strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
});
test('Inserts and Deletes at the same time', async () => {
let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
storage.set('bar', 'foo');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
await storage.close();
storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
storage.set('bar', 'foobar');
const largeItem = JSON.stringify({ largeItem: 'Hello World'.repeat(1000) });
storage.set('largeItem', largeItem);
storage.delete('barNumber');
storage.delete('barBoolean');
await storage.close();
storage = new Storage(await IndexedDBStorageDatabase.create(id, logService));
await storage.init();
strictEqual(storage.get('bar'), 'foobar');
strictEqual(storage.get('largeItem'), largeItem);
strictEqual(storage.get('barNumber'), undefined);
strictEqual(storage.get('barBoolean'), undefined);
});
});

View file

@ -56,8 +56,7 @@ export interface IOpenedWindow {
readonly dirty: boolean;
}
export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions {
}
export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { }
export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen;

View file

@ -331,7 +331,7 @@ class BrowserMain extends Disposable {
}
private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise<BrowserStorageService> {
const storageService = new BrowserStorageService(payload, environmentService, fileService);
const storageService = new BrowserStorageService(payload, logService, environmentService, fileService);
try {
await storageService.initialize();

View file

@ -146,11 +146,24 @@ export class BrowserWindow extends Disposable {
if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) {
const opened = windowOpenNoOpenerWithSuccess(href);
if (!opened) {
const showResult = await this.dialogService.show(Severity.Warning, localize('unableToOpenExternal', "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway."),
[localize('open', "Open"), localize('learnMore', "Learn More"), localize('cancel', "Cancel")], { cancelId: 2, detail: href });
const showResult = await this.dialogService.show(
Severity.Warning,
localize('unableToOpenExternal', "The browser interrupted the opening of a new tab or window. Press 'Open' to open it anyway."),
[
localize('open', "Open"),
localize('learnMore', "Learn More"),
localize('cancel', "Cancel")
],
{
cancelId: 2,
detail: href
}
);
if (showResult.choice === 0) {
windowOpenNoOpener(href);
}
if (showResult.choice === 1) {
await this.openerService.open(URI.parse('https://aka.ms/allow-vscode-popup'));
}
@ -165,13 +178,13 @@ export class BrowserWindow extends Disposable {
}
private registerLabelFormatters() {
this.labelService.registerFormatter({
this._register(this.labelService.registerFormatter({
scheme: Schemas.userData,
priority: true,
formatting: {
label: '(Settings) ${path}',
separator: '/',
}
});
}));
}
}