@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
297 lines (296 loc) • 10.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 });
}
}