@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
426 lines (425 loc) • 14.2 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 { 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);
}