sussudio
Version:
An unofficial VS Code Internal API
243 lines (242 loc) • 8.79 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 { ThrottledDelayer } from "../../../common/async.mjs";
import { Emitter, Event } from "../../../common/event.mjs";
import { Disposable } from "../../../common/lifecycle.mjs";
import { isUndefinedOrNull } from "../../../common/types.mjs";
export var StorageHint;
(function (StorageHint) {
// A hint to the storage that the storage
// does not exist on disk yet. This allows
// the storage library to improve startup
// time by not checking the storage for data.
StorageHint[StorageHint["STORAGE_DOES_NOT_EXIST"] = 0] = "STORAGE_DOES_NOT_EXIST";
// A hint to the storage that the storage
// is backed by an in-memory storage.
StorageHint[StorageHint["STORAGE_IN_MEMORY"] = 1] = "STORAGE_IN_MEMORY";
})(StorageHint || (StorageHint = {}));
export function isStorageItemsChangeEvent(thing) {
const candidate = thing;
return candidate?.changed instanceof Map || candidate?.deleted instanceof Set;
}
export var StorageState;
(function (StorageState) {
StorageState[StorageState["None"] = 0] = "None";
StorageState[StorageState["Initialized"] = 1] = "Initialized";
StorageState[StorageState["Closed"] = 2] = "Closed";
})(StorageState || (StorageState = {}));
export class Storage extends Disposable {
database;
options;
static DEFAULT_FLUSH_DELAY = 100;
_onDidChangeStorage = this._register(new Emitter());
onDidChangeStorage = this._onDidChangeStorage.event;
state = StorageState.None;
cache = new Map();
flushDelayer = new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY);
pendingDeletes = new Set();
pendingInserts = new Map();
pendingClose = undefined;
whenFlushedCallbacks = [];
constructor(database, options = Object.create(null)) {
super();
this.database = database;
this.options = options;
this.registerListeners();
}
registerListeners() {
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
}
onDidChangeItemsExternal(e) {
// items that change external require us to update our
// caches with the values. we just accept the value and
// emit an event if there is a change.
e.changed?.forEach((value, key) => this.accept(key, value));
e.deleted?.forEach(key => this.accept(key, undefined));
}
accept(key, value) {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
let changed = false;
// Item got removed, check for deletion
if (isUndefinedOrNull(value)) {
changed = this.cache.delete(key);
}
// Item got updated, check for change
else {
const currentValue = this.cache.get(key);
if (currentValue !== value) {
this.cache.set(key, value);
changed = true;
}
}
// Signal to outside listeners
if (changed) {
this._onDidChangeStorage.fire(key);
}
}
get items() {
return this.cache;
}
get size() {
return this.cache.size;
}
async init() {
if (this.state !== StorageState.None) {
return; // either closed or already initialized
}
this.state = StorageState.Initialized;
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
// return early if we know the storage file does not exist. this is a performance
// optimization to not load all items of the underlying storage if we know that
// there can be no items because the storage does not exist.
return;
}
this.cache = await this.database.getItems();
}
get(key, fallbackValue) {
const value = this.cache.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key, fallbackValue) {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getNumber(key, fallbackValue) {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
async set(key, value) {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.delete(key);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.cache.get(key);
if (currentValue === valueStr) {
return;
}
// Update in cache and pending
this.cache.set(key, valueStr);
this.pendingInserts.set(key, valueStr);
this.pendingDeletes.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.doFlush();
}
async delete(key) {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
// Remove from cache and add to pending
const wasDeleted = this.cache.delete(key);
if (!wasDeleted) {
return; // Return early if value already deleted
}
if (!this.pendingDeletes.has(key)) {
this.pendingDeletes.add(key);
}
this.pendingInserts.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.doFlush();
}
async close() {
if (!this.pendingClose) {
this.pendingClose = this.doClose();
}
return this.pendingClose;
}
async doClose() {
// Update state
this.state = StorageState.Closed;
// Trigger new flush to ensure data is persisted and then close
// even if there is an error flushing. We must always ensure
// the DB is closed to avoid corruption.
//
// Recovery: we pass our cache over as recovery option in case
// the DB is not healthy.
try {
await this.doFlush(0 /* as soon as possible */);
}
catch (error) {
// Ignore
}
await this.database.close(() => this.cache);
}
get hasPending() {
return this.pendingInserts.size > 0 || this.pendingDeletes.size > 0;
}
async flushPending() {
if (!this.hasPending) {
return; // return early if nothing to do
}
// Get pending data
const updateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
// Reset pending data for next run
this.pendingDeletes = new Set();
this.pendingInserts = new Map();
// Update in storage and release any
// waiters we have once done
return this.database.updateItems(updateRequest).finally(() => {
if (!this.hasPending) {
while (this.whenFlushedCallbacks.length) {
this.whenFlushedCallbacks.pop()?.();
}
}
});
}
async flush(delay) {
if (!this.hasPending) {
return; // return early if nothing to do
}
return this.doFlush(delay);
}
async doFlush(delay) {
return this.flushDelayer.trigger(() => this.flushPending(), delay);
}
async whenFlushed() {
if (!this.hasPending) {
return; // return early if nothing to do
}
return new Promise(resolve => this.whenFlushedCallbacks.push(resolve));
}
isInMemory() {
return this.options.hint === StorageHint.STORAGE_IN_MEMORY;
}
dispose() {
this.flushDelayer.dispose();
super.dispose();
}
}
export class InMemoryStorageDatabase {
onDidChangeItemsExternal = Event.None;
items = new Map();
async getItems() {
return this.items;
}
async updateItems(request) {
request.insert?.forEach((value, key) => this.items.set(key, value));
request.delete?.forEach(key => this.items.delete(key));
}
async close() { }
}