web - implemented indexedDB based storage service
This commit is contained in:
parent
4f3f431908
commit
ad60f647d9
|
@ -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": [
|
||||
|
|
19
src/vs/base/test/common/testUtils.ts
Normal file
19
src/vs/base/test/common/testUtils.ts
Normal 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);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: '/',
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue