import { readFile, writeFile, mkdir } from "fs"; import * as path from "path"; import { promisify } from "util"; import { IDisposable } from "@coder/disposable"; import { Event } from "vs/base/common/event"; import * as workspaceStorage from "vs/base/node/storage"; import * as globalStorage from "vs/platform/storage/node/storageIpc"; import * as paths from "./paths"; import { logger, field } from "@coder/logger"; import { client } from "@coder/vscode/src/client"; import { IStorageService, WillSaveStateReason } from "vs/platform/storage/common/storage"; class StorageDatabase implements workspaceStorage.IStorageDatabase { public readonly onDidChangeItemsExternal = Event.None; private readonly items = new Map(); private fetched: boolean = false; private readonly path: string; public constructor(path: string) { this.path = path.replace(/\.vscdb$/, ".json"); logger.debug("Setting up storage", field("path", this.path)); window.addEventListener("unload", () => { if (!navigator.sendBeacon) { throw new Error("cannot save state"); } this.triggerFlush(WillSaveStateReason.SHUTDOWN); navigator.sendBeacon(`/resource${this.path}`, this.content); }); } public async getItems(): Promise> { if (this.fetched) { return this.items; } try { const contents = await promisify(readFile)(this.path, "utf8"); const json = JSON.parse(contents); Object.keys(json).forEach((key) => { this.items.set(key, json[key]); }); } catch (error) { if (error.code !== "ENOENT") { throw error; } } this.fetched = true; return this.items; } public updateItems(request: workspaceStorage.IUpdateRequest): Promise { if (request.insert) { request.insert.forEach((value, key) => this.items.set(key, value)); } if (request.delete) { request.delete.forEach(key => this.items.delete(key)); } return this.save(); } public close(): Promise { return Promise.resolve(); } public checkIntegrity(): Promise { return Promise.resolve("ok"); } private async save(): Promise { try { await promisify(mkdir)(path.dirname(this.path)); } catch (ex) {} return promisify(writeFile)(this.path, this.content); } private triggerFlush(reason: WillSaveStateReason = WillSaveStateReason.NONE): boolean { // tslint:disable-next-line:no-any const storageService = client.serviceCollection.get(IStorageService) as any; if (reason === WillSaveStateReason.SHUTDOWN && storageService.close) { storageService.close(); return true; } if (storageService._onWillSaveState) { storageService._onWillSaveState.fire({ reason }); return true; } return false; } private get content(): string { const json: { [key: string]: string } = {}; this.items.forEach((value, key) => { json[key] = value; }); return JSON.stringify(json); } } class GlobalStorageDatabase extends StorageDatabase implements IDisposable { public constructor() { super(path.join(paths.getAppDataPath(), "globalStorage", "state.vscdb")); } public dispose(): void { // Nothing to do. } } const workspaceTarget = workspaceStorage as typeof workspaceStorage; // @ts-ignore TODO: don't ignore it. workspaceTarget.SQLiteStorageDatabase = StorageDatabase; const globalTarget = globalStorage as typeof globalStorage; // @ts-ignore TODO: don't ignore it. globalTarget.GlobalStorageDatabaseChannelClient = GlobalStorageDatabase;