UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

297 lines (296 loc) 10.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { top } from '@sussudio/base/common/arrays.mjs'; import { DeferredPromise } from '@sussudio/base/common/async.mjs'; import { Emitter } from '@sussudio/base/common/event.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { join } from '@sussudio/base/common/path.mjs'; import { StopWatch } from '@sussudio/base/common/stopwatch.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { Promises } from '@sussudio/base/node/pfs.mjs'; import { InMemoryStorageDatabase, Storage, StorageHint, StorageState, } from '@sussudio/base/parts/storage/common/storage.mjs'; import { SQLiteStorageDatabase } from '@sussudio/base/parts/storage/node/storage.mjs'; import { LogLevel } from '../../log/common/log.mjs'; import { IS_NEW_KEY } from '../common/storage.mjs'; import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, } from '../../telemetry/common/telemetry.mjs'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../workspace/common/workspace.mjs'; class BaseStorageMain extends Disposable { logService; fileService; static LOG_SLOW_CLOSE_THRESHOLD = 2000; _onDidChangeStorage = this._register(new Emitter()); onDidChangeStorage = this._onDidChangeStorage.event; _onDidCloseStorage = this._register(new Emitter()); onDidCloseStorage = this._onDidCloseStorage.event; _storage = new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); // storage is in-memory until initialized get storage() { return this._storage; } initializePromise = undefined; whenInitPromise = new DeferredPromise(); whenInit = this.whenInitPromise.p; state = StorageState.None; constructor(logService, fileService) { super(); this.logService = logService; this.fileService = fileService; } isInMemory() { return this._storage.isInMemory(); } init() { if (!this.initializePromise) { this.initializePromise = (async () => { if (this.state !== StorageState.None) { return; // either closed or already initialized } try { // Create storage via subclasses const storage = await this.doCreate(); // Replace our in-memory storage with the real // once as soon as possible without awaiting // the init call. this._storage.dispose(); this._storage = storage; // Re-emit storage changes via event this._register(storage.onDidChangeStorage((key) => this._onDidChangeStorage.fire({ key }))); // Await storage init await this.doInit(storage); // Ensure we track wether storage is new or not const isNewStorage = storage.getBoolean(IS_NEW_KEY); if (isNewStorage === undefined) { storage.set(IS_NEW_KEY, true); } else if (isNewStorage) { storage.set(IS_NEW_KEY, false); } } catch (error) { this.logService.error(`[storage main] initialize(): Unable to init storage due to ${error}`); } finally { // Update state this.state = StorageState.Initialized; // Mark init promise as completed this.whenInitPromise.complete(); } })(); } return this.initializePromise; } createLoggingOptions() { return { logTrace: this.logService.getLevel() === LogLevel.Trace ? (msg) => this.logService.trace(msg) : undefined, logError: (error) => this.logService.error(error), }; } doInit(storage) { return storage.init(); } get items() { return this._storage.items; } get(key, fallbackValue) { return this._storage.get(key, fallbackValue); } set(key, value) { return this._storage.set(key, value); } delete(key) { return this._storage.delete(key); } async close() { // Measure how long it takes to close storage const watch = new StopWatch(false); await this.doClose(); watch.stop(); // If close() is taking a long time, there is // a chance that the underlying DB is large // either on disk or in general. In that case // log some additional info to further diagnose if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { await this.logSlowClose(watch); } // Signal as event this._onDidCloseStorage.fire(); } async logSlowClose(watch) { if (!this.path) { return; } try { const largestEntries = top( Array.from(this._storage.items.entries()).map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5, ) .map((entry) => `${entry.key}:${entry.length}`) .join(', '); const dbSize = (await this.fileService.stat(URI.file(this.path))).size; this.logService.warn( `[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`, ); } catch (error) { this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); } } async doClose() { // Ensure we are not accidentally leaving // a pending initialized storage behind in // case `close()` was called before `init()` // finishes. if (this.initializePromise) { await this.initializePromise; } // Update state this.state = StorageState.Closed; // Propagate to storage lib await this._storage.close(); } } class BaseProfileAwareStorageMain extends BaseStorageMain { profile; options; static STORAGE_NAME = 'state.vscdb'; get path() { if (!this.options.useInMemoryStorage) { return join(this.profile.globalStorageHome.fsPath, BaseProfileAwareStorageMain.STORAGE_NAME); } return undefined; } constructor(profile, options, logService, fileService) { super(logService, fileService); this.profile = profile; this.options = options; } async doCreate() { return new Storage( new SQLiteStorageDatabase(this.path ?? SQLiteStorageDatabase.IN_MEMORY_PATH, { logging: this.createLoggingOptions(), }), !this.path ? { hint: StorageHint.STORAGE_IN_MEMORY } : undefined, ); } } export class ProfileStorageMain extends BaseProfileAwareStorageMain { constructor(profile, options, logService, fileService) { super(profile, options, logService, fileService); } } export class ApplicationStorageMain extends BaseProfileAwareStorageMain { constructor(options, userDataProfileService, logService, fileService) { super(userDataProfileService.defaultProfile, options, logService, fileService); } async doInit(storage) { await super.doInit(storage); // Apply telemetry values as part of the application storage initialization this.updateTelemetryState(storage); } updateTelemetryState(storage) { // First session date (once) const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined); if (firstSessionDate === undefined) { storage.set(firstSessionDateStorageKey, new Date().toUTCString()); } // Last / current session (always) // previous session date was the "current" one at that time // current session date is "now" const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined); const currentSessionDate = new Date().toUTCString(); storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); storage.set(currentSessionDateStorageKey, currentSessionDate); } } export class WorkspaceStorageMain extends BaseStorageMain { workspace; options; environmentService; static WORKSPACE_STORAGE_NAME = 'state.vscdb'; static WORKSPACE_META_NAME = 'workspace.json'; get path() { if (!this.options.useInMemoryStorage) { return join( this.environmentService.workspaceStorageHome.fsPath, this.workspace.id, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME, ); } return undefined; } constructor(workspace, options, logService, environmentService, fileService) { super(logService, fileService); this.workspace = workspace; this.options = options; this.environmentService = environmentService; } async doCreate() { const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder(); return new Storage( new SQLiteStorageDatabase(storageFilePath, { logging: this.createLoggingOptions(), }), { hint: this.options.useInMemoryStorage ? StorageHint.STORAGE_IN_MEMORY : wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined, }, ); } async prepareWorkspaceStorageFolder() { // Return early if using inMemory storage if (this.options.useInMemoryStorage) { return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true }; } // Otherwise, ensure the storage folder exists on disk const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id); const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); const storageExists = await Promises.exists(workspaceStorageFolderPath); if (storageExists) { return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false }; } // Ensure storage folder exists await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true }; } async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath) { let meta = undefined; if (isSingleFolderWorkspaceIdentifier(this.workspace)) { meta = { folder: this.workspace.uri.toString() }; } else if (isWorkspaceIdentifier(this.workspace)) { meta = { workspace: this.workspace.configPath.toString() }; } if (meta) { try { const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME); const storageExists = await Promises.exists(workspaceStorageMetaPath); if (!storageExists) { await Promises.writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { this.logService.error( `[storage main] ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`, ); } } } } export class InMemoryStorageMain extends BaseStorageMain { get path() { return undefined; // in-memory has no path } async doCreate() { return new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); } }