UNPKG

sussudio

Version:

An unofficial VS Code Internal API

243 lines (242 loc) 8.79 kB
/*--------------------------------------------------------------------------------------------- * 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() { } }