UNPKG

@sussudio/platform

Version:

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

426 lines (425 loc) 14.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Promises, RunOnceScheduler, runWhenIdle } from '@sussudio/base/common/async.mjs'; import { Emitter, PauseableEmitter } from '@sussudio/base/common/event.mjs'; import { Disposable, dispose, MutableDisposable } from '@sussudio/base/common/lifecycle.mjs'; import { mark } from '@sussudio/base/common/performance.mjs'; import { isUndefinedOrNull } from '@sussudio/base/common/types.mjs'; import { InMemoryStorageDatabase, Storage, StorageHint } from '@sussudio/base/parts/storage/common/storage.mjs'; import { createDecorator } from '../../instantiation/common/instantiation.mjs'; import { isUserDataProfile } from '../../userDataProfile/common/userDataProfile.mjs'; export const IS_NEW_KEY = '__$__isNewStorageMarker'; export const TARGET_KEY = '__$__targetStorageMarker'; export const IStorageService = createDecorator('storageService'); export var WillSaveStateReason; (function (WillSaveStateReason) { /** * No specific reason to save state. */ WillSaveStateReason[(WillSaveStateReason['NONE'] = 0)] = 'NONE'; /** * A hint that the workbench is about to shutdown. */ WillSaveStateReason[(WillSaveStateReason['SHUTDOWN'] = 1)] = 'SHUTDOWN'; })(WillSaveStateReason || (WillSaveStateReason = {})); export function loadKeyTargets(storage) { const keysRaw = storage.get(TARGET_KEY); if (keysRaw) { try { return JSON.parse(keysRaw); } catch (error) { // Fail gracefully } } return Object.create(null); } export class AbstractStorageService extends Disposable { options; static DEFAULT_FLUSH_INTERVAL = 60 * 1000; // every minute _onDidChangeValue = this._register(new PauseableEmitter()); onDidChangeValue = this._onDidChangeValue.event; _onDidChangeTarget = this._register(new PauseableEmitter()); onDidChangeTarget = this._onDidChangeTarget.event; _onWillSaveState = this._register(new Emitter()); onWillSaveState = this._onWillSaveState.event; initializationPromise; flushWhenIdleScheduler = this._register( new RunOnceScheduler(() => this.doFlushWhenIdle(), this.options.flushInterval), ); runFlushWhenIdle = this._register(new MutableDisposable()); constructor(options = { flushInterval: AbstractStorageService.DEFAULT_FLUSH_INTERVAL }) { super(); this.options = options; } doFlushWhenIdle() { this.runFlushWhenIdle.value = runWhenIdle(() => { if (this.shouldFlushWhenIdle()) { this.flush(); } // repeat this.flushWhenIdleScheduler.schedule(); }); } shouldFlushWhenIdle() { return true; } stopFlushWhenIdle() { dispose([this.runFlushWhenIdle, this.flushWhenIdleScheduler]); } initialize() { if (!this.initializationPromise) { this.initializationPromise = (async () => { // Init all storage locations mark('code/willInitStorage'); try { await this.doInitialize(); // Ask subclasses to initialize storage } finally { mark('code/didInitStorage'); } // On some OS we do not get enough time to persist state on shutdown (e.g. when // Windows restarts after applying updates). In other cases, VSCode might crash, // so we periodically save state to reduce the chance of loosing any state. // In the browser we do not have support for long running unload sequences. As such, // we cannot ask for saving state in that moment, because that would result in a // long running operation. // Instead, periodically ask customers to save save. The library will be clever enough // to only save state that has actually changed. this.flushWhenIdleScheduler.schedule(); })(); } return this.initializationPromise; } emitDidChangeValue(scope, key) { // Specially handle `TARGET_KEY` if (key === TARGET_KEY) { // Clear our cached version which is now out of date switch (scope) { case -1 /* StorageScope.APPLICATION */: this._applicationKeyTargets = undefined; break; case 0 /* StorageScope.PROFILE */: this._profileKeyTargets = undefined; break; case 1 /* StorageScope.WORKSPACE */: this._workspaceKeyTargets = undefined; break; } // Emit as `didChangeTarget` event this._onDidChangeTarget.fire({ scope }); } // Emit any other key to outside else { this._onDidChangeValue.fire({ scope, key, target: this.getKeyTargets(scope)[key] }); } } emitWillSaveState(reason) { this._onWillSaveState.fire({ reason }); } get(key, scope, fallbackValue) { return this.getStorage(scope)?.get(key, fallbackValue); } getBoolean(key, scope, fallbackValue) { return this.getStorage(scope)?.getBoolean(key, fallbackValue); } getNumber(key, scope, fallbackValue) { return this.getStorage(scope)?.getNumber(key, fallbackValue); } store(key, value, scope, target) { // We remove the key for undefined/null values if (isUndefinedOrNull(value)) { this.remove(key, scope); return; } // Update our datastructures but send events only after this.withPausedEmitters(() => { // Update key-target map this.updateKeyTarget(key, scope, target); // Store actual value this.getStorage(scope)?.set(key, value); }); } remove(key, scope) { // Update our datastructures but send events only after this.withPausedEmitters(() => { // Update key-target map this.updateKeyTarget(key, scope, undefined); // Remove actual key this.getStorage(scope)?.delete(key); }); } withPausedEmitters(fn) { // Pause emitters this._onDidChangeValue.pause(); this._onDidChangeTarget.pause(); try { fn(); } finally { // Resume emitters this._onDidChangeValue.resume(); this._onDidChangeTarget.resume(); } } keys(scope, target) { const keys = []; const keyTargets = this.getKeyTargets(scope); for (const key of Object.keys(keyTargets)) { const keyTarget = keyTargets[key]; if (keyTarget === target) { keys.push(key); } } return keys; } updateKeyTarget(key, scope, target) { // Add const keyTargets = this.getKeyTargets(scope); if (typeof target === 'number') { if (keyTargets[key] !== target) { keyTargets[key] = target; this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets)); } } // Remove else { if (typeof keyTargets[key] === 'number') { delete keyTargets[key]; this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets)); } } } _workspaceKeyTargets = undefined; get workspaceKeyTargets() { if (!this._workspaceKeyTargets) { this._workspaceKeyTargets = this.loadKeyTargets(1 /* StorageScope.WORKSPACE */); } return this._workspaceKeyTargets; } _profileKeyTargets = undefined; get profileKeyTargets() { if (!this._profileKeyTargets) { this._profileKeyTargets = this.loadKeyTargets(0 /* StorageScope.PROFILE */); } return this._profileKeyTargets; } _applicationKeyTargets = undefined; get applicationKeyTargets() { if (!this._applicationKeyTargets) { this._applicationKeyTargets = this.loadKeyTargets(-1 /* StorageScope.APPLICATION */); } return this._applicationKeyTargets; } getKeyTargets(scope) { switch (scope) { case -1 /* StorageScope.APPLICATION */: return this.applicationKeyTargets; case 0 /* StorageScope.PROFILE */: return this.profileKeyTargets; default: return this.workspaceKeyTargets; } } loadKeyTargets(scope) { const storage = this.getStorage(scope); return storage ? loadKeyTargets(storage) : Object.create(null); } isNew(scope) { return this.getBoolean(IS_NEW_KEY, scope) === true; } async flush(reason = WillSaveStateReason.NONE) { // Signal event to collect changes this._onWillSaveState.fire({ reason }); const applicationStorage = this.getStorage(-1 /* StorageScope.APPLICATION */); const profileStorage = this.getStorage(0 /* StorageScope.PROFILE */); const workspaceStorage = this.getStorage(1 /* StorageScope.WORKSPACE */); switch (reason) { // Unspecific reason: just wait when data is flushed case WillSaveStateReason.NONE: await Promises.settled([ applicationStorage?.whenFlushed() ?? Promise.resolve(), profileStorage?.whenFlushed() ?? Promise.resolve(), workspaceStorage?.whenFlushed() ?? Promise.resolve(), ]); break; // Shutdown: we want to flush as soon as possible // and not hit any delays that might be there case WillSaveStateReason.SHUTDOWN: await Promises.settled([ applicationStorage?.flush(0) ?? Promise.resolve(), profileStorage?.flush(0) ?? Promise.resolve(), workspaceStorage?.flush(0) ?? Promise.resolve(), ]); break; } } async log() { const applicationItems = this.getStorage(-1 /* StorageScope.APPLICATION */)?.items ?? new Map(); const profileItems = this.getStorage(0 /* StorageScope.PROFILE */)?.items ?? new Map(); const workspaceItems = this.getStorage(1 /* StorageScope.WORKSPACE */)?.items ?? new Map(); return logStorage( applicationItems, profileItems, workspaceItems, this.getLogDetails(-1 /* StorageScope.APPLICATION */) ?? '', this.getLogDetails(0 /* StorageScope.PROFILE */) ?? '', this.getLogDetails(1 /* StorageScope.WORKSPACE */) ?? '', ); } async switch(to, preserveData) { // Signal as event so that clients can store data before we switch this.emitWillSaveState(WillSaveStateReason.NONE); if (isUserDataProfile(to)) { return this.switchToProfile(to, preserveData); } return this.switchToWorkspace(to, preserveData); } canSwitchProfile(from, to) { if (from.id === to.id) { return false; // both profiles are same } if (isProfileUsingDefaultStorage(to) && isProfileUsingDefaultStorage(from)) { return false; // both profiles are using default } return true; } switchData(oldStorage, newStorage, scope, preserveData) { this.withPausedEmitters(() => { // Copy over previous keys if `preserveData` if (preserveData) { for (const [key, value] of oldStorage) { newStorage.set(key, value); } } // Otherwise signal storage keys that have changed else { const handledkeys = new Set(); for (const [key, oldValue] of oldStorage) { handledkeys.add(key); const newValue = newStorage.get(key); if (newValue !== oldValue) { this.emitDidChangeValue(scope, key); } } for (const [key] of newStorage.items) { if (!handledkeys.has(key)) { this.emitDidChangeValue(scope, key); } } } }); } } export function isProfileUsingDefaultStorage(profile) { return profile.isDefault || !!profile.useDefaultFlags?.uiState; } export class InMemoryStorageService extends AbstractStorageService { applicationStorage = this._register( new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }), ); profileStorage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY })); workspaceStorage = this._register( new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }), ); constructor() { super(); this._register( this.workspaceStorage.onDidChangeStorage((key) => this.emitDidChangeValue(1 /* StorageScope.WORKSPACE */, key)), ); this._register( this.profileStorage.onDidChangeStorage((key) => this.emitDidChangeValue(0 /* StorageScope.PROFILE */, key)), ); this._register( this.applicationStorage.onDidChangeStorage((key) => this.emitDidChangeValue(-1 /* StorageScope.APPLICATION */, key), ), ); } getStorage(scope) { switch (scope) { case -1 /* StorageScope.APPLICATION */: return this.applicationStorage; case 0 /* StorageScope.PROFILE */: return this.profileStorage; default: return this.workspaceStorage; } } getLogDetails(scope) { switch (scope) { case -1 /* StorageScope.APPLICATION */: return 'inMemory (application)'; case 0 /* StorageScope.PROFILE */: return 'inMemory (profile)'; default: return 'inMemory (workspace)'; } } async doInitialize() {} async switchToProfile() { // no-op when in-memory } async switchToWorkspace() { // no-op when in-memory } hasScope(scope) { return false; } } export async function logStorage(application, profile, workspace, applicationPath, profilePath, workspacePath) { const safeParse = (value) => { try { return JSON.parse(value); } catch (error) { return value; } }; const applicationItems = new Map(); const applicationItemsParsed = new Map(); application.forEach((value, key) => { applicationItems.set(key, value); applicationItemsParsed.set(key, safeParse(value)); }); const profileItems = new Map(); const profileItemsParsed = new Map(); profile.forEach((value, key) => { profileItems.set(key, value); profileItemsParsed.set(key, safeParse(value)); }); const workspaceItems = new Map(); const workspaceItemsParsed = new Map(); workspace.forEach((value, key) => { workspaceItems.set(key, value); workspaceItemsParsed.set(key, safeParse(value)); }); if (applicationPath !== profilePath) { console.group(`Storage: Application (path: ${applicationPath})`); } else { console.group(`Storage: Application & Profile (path: ${applicationPath}, default profile)`); } const applicationValues = []; applicationItems.forEach((value, key) => { applicationValues.push({ key, value }); }); console.table(applicationValues); console.groupEnd(); console.log(applicationItemsParsed); if (applicationPath !== profilePath) { console.group(`Storage: Profile (path: ${profilePath}, profile specific)`); const profileValues = []; profileItems.forEach((value, key) => { profileValues.push({ key, value }); }); console.table(profileValues); console.groupEnd(); console.log(profileItemsParsed); } console.group(`Storage: Workspace (path: ${workspacePath})`); const workspaceValues = []; workspaceItems.forEach((value, key) => { workspaceValues.push({ key, value }); }); console.table(workspaceValues); console.groupEnd(); console.log(workspaceItemsParsed); }