mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 09:35:41 -05:00
Merge VS Code 1.31.1 (#4283)
This commit is contained in:
@@ -83,7 +83,7 @@ export function getFirstFrame(arg0: IRemoteConsoleLog | string | undefined): ISt
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findFirstFrame(stack: string | undefined): string | undefined {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as crypto from 'crypto';
|
||||
import * as stream from 'stream';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
export function checksum(path: string, sha1hash: string): Promise<void> {
|
||||
export function checksum(path: string, sha1hash: string | undefined): Promise<void> {
|
||||
const promise = new Promise<string | undefined>((c, e) => {
|
||||
const input = fs.createReadStream(path);
|
||||
const hash = crypto.createHash('sha1');
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as stream from 'vs/base/node/stream';
|
||||
import * as iconv from 'iconv-lite';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { exec } from 'child_process';
|
||||
import { Readable, Writable, WritableOptions } from 'stream';
|
||||
import { Readable, Writable } from 'stream';
|
||||
|
||||
export const UTF8 = 'utf8';
|
||||
export const UTF8_with_bom = 'utf8bom';
|
||||
@@ -36,15 +36,10 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions
|
||||
readable.pipe(new class extends Writable {
|
||||
|
||||
private _decodeStream: NodeJS.ReadWriteStream;
|
||||
private _decodeStreamConstruction: Thenable<any>;
|
||||
private _decodeStreamConstruction: Promise<any>;
|
||||
private _buffer: Buffer[] = [];
|
||||
private _bytesBuffered = 0;
|
||||
|
||||
constructor(opts?: WritableOptions) {
|
||||
super(opts);
|
||||
this.once('finish', () => this._finish());
|
||||
}
|
||||
|
||||
_write(chunk: any, encoding: string, callback: Function): void {
|
||||
if (!Buffer.isBuffer(chunk)) {
|
||||
callback(new Error('data must be a buffer'));
|
||||
@@ -93,14 +88,13 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
_finish(): void {
|
||||
_final(callback: (err?: any) => any) {
|
||||
if (this._decodeStream) {
|
||||
// normal finish
|
||||
this._decodeStream.end();
|
||||
this._decodeStream.end(callback);
|
||||
} else {
|
||||
// we were still waiting for data...
|
||||
this._startDecodeStream(() => this._decodeStream.end());
|
||||
this._startDecodeStream(() => this._decodeStream.end(callback));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -393,15 +387,14 @@ export function resolveTerminalEncoding(verbose?: boolean): Promise<string> {
|
||||
exec('chcp', (err, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings);
|
||||
for (let i = 0; i < windowsTerminalEncodingKeys.length; i++) {
|
||||
const key = windowsTerminalEncodingKeys[i];
|
||||
for (const key of windowsTerminalEncodingKeys) {
|
||||
if (stdout.indexOf(key) >= 0) {
|
||||
return resolve(windowsTerminalEncodings[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(void 0);
|
||||
return resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ function doCopyFile(source: string, target: string, mode: number, callback: (err
|
||||
|
||||
export function mkdirp(path: string, mode?: number, token?: CancellationToken): Promise<boolean> {
|
||||
const mkdir = (): Promise<null> => {
|
||||
return nfcall(fs.mkdir, path, mode).then(null, (mkdirErr: NodeJS.ErrnoException) => {
|
||||
return nfcall(fs.mkdir, path, mode).then(undefined, (mkdirErr: NodeJS.ErrnoException) => {
|
||||
|
||||
// ENOENT: a parent folder does not exist yet
|
||||
if (mkdirErr.code === 'ENOENT') {
|
||||
@@ -155,7 +155,7 @@ export function mkdirp(path: string, mode?: number, token?: CancellationToken):
|
||||
}
|
||||
|
||||
// recursively mkdir
|
||||
return mkdir().then(null, (err: NodeJS.ErrnoException) => {
|
||||
return mkdir().then(undefined, (err: NodeJS.ErrnoException) => {
|
||||
|
||||
// Respect cancellation
|
||||
if (token && token.isCancellationRequested) {
|
||||
@@ -390,7 +390,7 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream,
|
||||
if (error) {
|
||||
if (isOpen) {
|
||||
writer.once('close', () => callback(error));
|
||||
writer.close();
|
||||
writer.destroy();
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
@@ -450,10 +450,10 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream,
|
||||
canFlush = false;
|
||||
}
|
||||
|
||||
writer.close();
|
||||
writer.destroy();
|
||||
});
|
||||
} else {
|
||||
writer.close();
|
||||
writer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as assert from 'assert';
|
||||
* Executes the given function (fn) over the given array of items (list) in parallel and returns the resulting errors and results as
|
||||
* array to the callback (callback). The resulting errors and results are evaluated by calling the provided callback function.
|
||||
*/
|
||||
export function parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error, result: E) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
|
||||
export function parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
|
||||
let results = new Array(list.length);
|
||||
let errors = new Array<Error | null>(list.length);
|
||||
let didErrorOccur = false;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { nfcall, Queue } from 'vs/base/common/async';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { once } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export function readdir(path: string): Promise<string[]> {
|
||||
return nfcall(extfs.readdir, path);
|
||||
@@ -36,7 +36,7 @@ export function rimraf(path: string): Promise<void> {
|
||||
}
|
||||
}, (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
@@ -77,12 +77,6 @@ export function unlink(path: string): Promise<void> {
|
||||
return nfcall(fs.unlink, path);
|
||||
}
|
||||
|
||||
export function unlinkIgnoreError(path: string): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
fs.unlink(path, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
export function symlink(target: string, path: string, type?: string): Promise<void> {
|
||||
return nfcall<void>(fs.symlink, target, path, type);
|
||||
}
|
||||
@@ -132,7 +126,7 @@ function ensureWriteFileQueue(queueKey: string): Queue<void> {
|
||||
writeFileQueue = new Queue<void>();
|
||||
writeFilePathQueue[queueKey] = writeFileQueue;
|
||||
|
||||
const onFinish = once(writeFileQueue.onFinished);
|
||||
const onFinish = Event.once(writeFileQueue.onFinished);
|
||||
onFinish(() => {
|
||||
delete writeFilePathQueue[queueKey];
|
||||
writeFileQueue.dispose();
|
||||
@@ -194,7 +188,7 @@ export function whenDeleted(path: string): Promise<void> {
|
||||
|
||||
if (!exists) {
|
||||
clearInterval(interval);
|
||||
resolve(void 0);
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function randomPort(): number {
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Thenable<number> {
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, Te
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode };
|
||||
|
||||
export type ValueCallback<T> = (value?: T | Thenable<T>) => void;
|
||||
export type ValueCallback<T> = (value?: T | Promise<T>) => void;
|
||||
export type ErrorCallback = (error?: any) => void;
|
||||
export type ProgressCallback<T> = (progress: T) => void;
|
||||
|
||||
@@ -72,6 +72,26 @@ export function getWindowsShell(): string {
|
||||
return process.env['comspec'] || 'cmd.exe';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a VS Code process environment by removing all Electron/VS Code-related values.
|
||||
*/
|
||||
export function sanitizeProcessEnvironment(env: Platform.IProcessEnvironment): void {
|
||||
const keysToRemove = [
|
||||
/^ELECTRON_.+$/,
|
||||
/^GOOGLE_API_KEY$/,
|
||||
/^VSCODE_.+$/
|
||||
];
|
||||
const envKeys = Object.keys(env);
|
||||
envKeys.forEach(envKey => {
|
||||
for (let i = 0; i < keysToRemove.length; i++) {
|
||||
if (envKey.search(keysToRemove[i]) !== -1) {
|
||||
delete env[envKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export abstract class AbstractProcess<TProgressData> {
|
||||
private cmd: string;
|
||||
private args: string[];
|
||||
@@ -107,7 +127,7 @@ export abstract class AbstractProcess<TProgressData> {
|
||||
public constructor(executable: Executable);
|
||||
public constructor(cmd: string, args: string[] | undefined, shell: boolean, options: CommandOptions | undefined);
|
||||
public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean, arg4?: CommandOptions) {
|
||||
if (arg2 !== void 0 && arg3 !== void 0 && arg4 !== void 0) {
|
||||
if (arg2 !== undefined && arg3 !== undefined && arg4 !== undefined) {
|
||||
this.cmd = <string>arg1;
|
||||
this.args = arg2;
|
||||
this.shell = arg3;
|
||||
@@ -409,7 +429,7 @@ export namespace win32 {
|
||||
if (path.isAbsolute(command)) {
|
||||
return command;
|
||||
}
|
||||
if (cwd === void 0) {
|
||||
if (cwd === undefined) {
|
||||
cwd = process.cwd();
|
||||
}
|
||||
let dir = path.dirname(command);
|
||||
@@ -418,11 +438,11 @@ export namespace win32 {
|
||||
// to the current working directory.
|
||||
return path.join(cwd, command);
|
||||
}
|
||||
if (paths === void 0 && Types.isString(process.env.PATH)) {
|
||||
if (paths === undefined && Types.isString(process.env.PATH)) {
|
||||
paths = process.env.PATH.split(path.delimiter);
|
||||
}
|
||||
// No PATH environment. Make path absolute to the cwd.
|
||||
if (paths === void 0 || paths.length === 0) {
|
||||
if (paths === undefined || paths.length === 0) {
|
||||
return path.join(cwd, command);
|
||||
}
|
||||
// We have a simple file name. We get the path variable from the env
|
||||
|
||||
@@ -22,7 +22,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let rootItem: ProcessItem;
|
||||
let rootItem: ProcessItem | undefined;
|
||||
const map = new Map<number, ProcessItem>();
|
||||
|
||||
function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) {
|
||||
@@ -109,7 +109,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
} while (matches);
|
||||
|
||||
if (result) {
|
||||
if (cmd.indexOf('node ') !== 0) {
|
||||
if (cmd.indexOf('node ') < 0 && cmd.indexOf('node.exe') < 0) {
|
||||
return `electron_node ${result}`;
|
||||
}
|
||||
}
|
||||
@@ -218,7 +218,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
|
||||
} else {
|
||||
const cpuUsage = stdout.toString().split('\n');
|
||||
for (let i = 0; i < pids.length; i++) {
|
||||
const processInfo = map.get(pids[i]);
|
||||
const processInfo = map.get(pids[i])!;
|
||||
processInfo.load = parseFloat(cpuUsage[i]);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export function request(options: IRequestOptions, token: CancellationToken): Pro
|
||||
opts.auth = options.user + ':' + options.password;
|
||||
}
|
||||
|
||||
req = rawRequest(opts, (res: http.ClientResponse) => {
|
||||
req = rawRequest(opts, (res: http.IncomingMessage) => {
|
||||
const followRedirects: number = isNumber(options.followRedirects) ? options.followRedirects : 3;
|
||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers['location']) {
|
||||
request(assign({}, options, {
|
||||
@@ -136,7 +136,7 @@ export function download(filePath: string, context: IRequestContext): Promise<vo
|
||||
return new Promise<void>((c, e) => {
|
||||
const out = createWriteStream(filePath);
|
||||
|
||||
out.once('finish', () => c(void 0));
|
||||
out.once('finish', () => c(undefined));
|
||||
context.stream.once('error', e);
|
||||
context.stream.pipe(out);
|
||||
});
|
||||
|
||||
@@ -49,9 +49,8 @@ export function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[
|
||||
const type = each['type'];
|
||||
if (type) {
|
||||
if (launchConfigs.has(type)) {
|
||||
launchConfigs.set(type, launchConfigs.get(type) + 1);
|
||||
}
|
||||
else {
|
||||
launchConfigs.set(type, launchConfigs.get(type)! + 1);
|
||||
} else {
|
||||
launchConfigs.set(type, 1);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +150,7 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise
|
||||
|
||||
let addFileType = (fileType: string) => {
|
||||
if (fileTypes.has(fileType)) {
|
||||
fileTypes.set(fileType, fileTypes.get(fileType) + 1);
|
||||
fileTypes.set(fileType, fileTypes.get(fileType)! + 1);
|
||||
}
|
||||
else {
|
||||
fileTypes.set(fileType, 1);
|
||||
@@ -162,7 +161,7 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise
|
||||
for (const each of configFilePatterns) {
|
||||
if (each.pattern.test(fileName)) {
|
||||
if (configFiles.has(each.tag)) {
|
||||
configFiles.set(each.tag, configFiles.get(each.tag) + 1);
|
||||
configFiles.set(each.tag, configFiles.get(each.tag)! + 1);
|
||||
} else {
|
||||
configFiles.set(each.tag, 1);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { mapToString, setToString } from 'vs/base/common/map';
|
||||
import { basename } from 'path';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
import { rename, unlinkIgnoreError, copy, renameIgnoreError } from 'vs/base/node/pfs';
|
||||
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
|
||||
import { fill } from 'vs/base/common/arrays';
|
||||
|
||||
export enum StorageHint {
|
||||
|
||||
@@ -39,12 +40,12 @@ export interface IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
|
||||
|
||||
getItems(): Thenable<Map<string, string>>;
|
||||
updateItems(request: IUpdateRequest): Thenable<void>;
|
||||
getItems(): Promise<Map<string, string>>;
|
||||
updateItems(request: IUpdateRequest): Promise<void>;
|
||||
|
||||
close(): Thenable<void>;
|
||||
close(recovery?: () => Map<string, string>): Promise<void>;
|
||||
|
||||
checkIntegrity(full: boolean): Thenable<string>;
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IStorage extends IDisposable {
|
||||
@@ -53,7 +54,7 @@ export interface IStorage extends IDisposable {
|
||||
readonly size: number;
|
||||
readonly onDidChangeStorage: Event<string>;
|
||||
|
||||
init(): Thenable<void>;
|
||||
init(): Promise<void>;
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
@@ -64,13 +65,12 @@ export interface IStorage extends IDisposable {
|
||||
getInteger(key: string, fallbackValue: number): number;
|
||||
getInteger(key: string, fallbackValue?: number): number | undefined;
|
||||
|
||||
set(key: string, value: any): Thenable<void>;
|
||||
delete(key: string): Thenable<void>;
|
||||
set(key: string, value: string | boolean | number): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
beforeClose(): void;
|
||||
close(): Thenable<void>;
|
||||
close(): Promise<void>;
|
||||
|
||||
checkIntegrity(full: boolean): Thenable<string>;
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
enum StorageState {
|
||||
@@ -92,7 +92,6 @@ export class Storage extends Disposable implements IStorage {
|
||||
private cache: Map<string, string> = new Map<string, string>();
|
||||
|
||||
private flushDelayer: ThrottledDelayer<void>;
|
||||
private flushDelay = Storage.DEFAULT_FLUSH_DELAY;
|
||||
|
||||
private pendingDeletes: Set<string> = new Set<string>();
|
||||
private pendingInserts: Map<string, string> = new Map();
|
||||
@@ -103,7 +102,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.flushDelayer = this._register(new ThrottledDelayer(this.flushDelay));
|
||||
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
@@ -154,7 +153,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
init(): Thenable<void> {
|
||||
init(): Promise<void> {
|
||||
if (this.state !== StorageState.None) {
|
||||
return Promise.resolve(); // either closed or already initialized
|
||||
}
|
||||
@@ -209,7 +208,7 @@ export class Storage extends Disposable implements IStorage {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
set(key: string, value: any): Thenable<void> {
|
||||
set(key: string, value: string | boolean | number): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
@@ -237,10 +236,10 @@ export class Storage extends Disposable implements IStorage {
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending(), this.flushDelay);
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
delete(key: string): Thenable<void> {
|
||||
delete(key: string): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
@@ -261,14 +260,10 @@ export class Storage extends Disposable implements IStorage {
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending(), this.flushDelay);
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
beforeClose(): void {
|
||||
this.flushDelay = 0; // when we are about to close, reduce our flush delay to 0 to consume too much time
|
||||
}
|
||||
|
||||
close(): Thenable<void> {
|
||||
close(): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // return if already closed
|
||||
}
|
||||
@@ -279,11 +274,14 @@ export class Storage extends Disposable implements IStorage {
|
||||
// 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.
|
||||
const onDone = () => this.database.close();
|
||||
//
|
||||
// Recovery: we pass our cache over as recovery option in case
|
||||
// the DB is not healthy.
|
||||
const onDone = () => this.database.close(() => this.cache);
|
||||
return this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */).then(onDone, onDone);
|
||||
}
|
||||
|
||||
private flushPending(): Thenable<void> {
|
||||
private flushPending(): Promise<void> {
|
||||
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
|
||||
return Promise.resolve(); // return early if nothing to do
|
||||
}
|
||||
@@ -299,14 +297,18 @@ export class Storage extends Disposable implements IStorage {
|
||||
return this.database.updateItems(updateRequest);
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Thenable<string> {
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return this.database.checkIntegrity(full);
|
||||
}
|
||||
}
|
||||
|
||||
interface IOpenDatabaseResult {
|
||||
interface IDatabaseConnection {
|
||||
db: Database;
|
||||
path: string;
|
||||
|
||||
isInMemory: boolean;
|
||||
|
||||
isErroneous?: boolean;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface ISQLiteStorageDatabaseOptions {
|
||||
@@ -327,24 +329,29 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
private static measuredRequireDuration: boolean; // TODO@Ben remove me after a while
|
||||
|
||||
private static BUSY_OPEN_TIMEOUT = 2000; // timeout in ms to retry when opening DB fails with SQLITE_BUSY
|
||||
private static MAX_HOST_PARAMETERS = 256; // maximum number of parameters within a statement
|
||||
|
||||
private path: string;
|
||||
private name: string;
|
||||
|
||||
private logger: SQLiteStorageDatabaseLogger;
|
||||
|
||||
private whenOpened: Promise<IOpenDatabaseResult>;
|
||||
private whenConnected: Promise<IDatabaseConnection>;
|
||||
|
||||
constructor(path: string, options: ISQLiteStorageDatabaseOptions = Object.create(null)) {
|
||||
this.path = path;
|
||||
this.name = basename(path);
|
||||
|
||||
this.logger = new SQLiteStorageDatabaseLogger(options.logging);
|
||||
|
||||
this.whenOpened = this.open(path);
|
||||
this.whenConnected = this.connect(path);
|
||||
}
|
||||
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return this.whenOpened.then(({ db }) => {
|
||||
return this.whenConnected.then(connection => {
|
||||
const items = new Map<string, string>();
|
||||
|
||||
return this.all(db, 'SELECT * FROM ItemTable').then(rows => {
|
||||
return this.all(connection, 'SELECT * FROM ItemTable').then(rows => {
|
||||
rows.forEach(row => items.set(row.key, row.value));
|
||||
|
||||
if (this.logger.isTracing) {
|
||||
@@ -357,6 +364,10 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
return this.whenConnected.then(connection => this.doUpdateItems(connection, request));
|
||||
}
|
||||
|
||||
private doUpdateItems(connection: IDatabaseConnection, request: IUpdateRequest): Promise<void> {
|
||||
let updateCount = 0;
|
||||
if (request.insert) {
|
||||
updateCount += request.insert.size;
|
||||
@@ -373,64 +384,148 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
this.logger.trace(`[storage ${this.name}] updateItems(): insert(${request.insert ? mapToString(request.insert) : '0'}), delete(${request.delete ? setToString(request.delete) : '0'})`);
|
||||
}
|
||||
|
||||
return this.whenOpened.then(({ db }) => {
|
||||
return this.transaction(db, () => {
|
||||
if (request.insert && request.insert.size > 0) {
|
||||
this.prepare(db, 'INSERT INTO ItemTable VALUES (?,?)', stmt => {
|
||||
request.insert!.forEach((value, key) => {
|
||||
stmt.run([key, value]);
|
||||
});
|
||||
});
|
||||
}
|
||||
return this.transaction(connection, () => {
|
||||
|
||||
if (request.delete && request.delete.size) {
|
||||
this.prepare(db, 'DELETE FROM ItemTable WHERE key=?', stmt => {
|
||||
request.delete!.forEach(key => {
|
||||
stmt.run(key);
|
||||
// INSERT
|
||||
if (request.insert && request.insert.size > 0) {
|
||||
const keysValuesChunks: (string[])[] = [];
|
||||
keysValuesChunks.push([]); // seed with initial empty chunk
|
||||
|
||||
// Split key/values into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
|
||||
// so that we can efficiently run the INSERT with as many HOST parameters as possible
|
||||
let currentChunkIndex = 0;
|
||||
request.insert.forEach((value, key) => {
|
||||
let keyValueChunk = keysValuesChunks[currentChunkIndex];
|
||||
|
||||
if (keyValueChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
|
||||
currentChunkIndex++;
|
||||
keyValueChunk = [];
|
||||
keysValuesChunks.push(keyValueChunk);
|
||||
}
|
||||
|
||||
keyValueChunk.push(key, value);
|
||||
});
|
||||
|
||||
keysValuesChunks.forEach(keysValuesChunk => {
|
||||
this.prepare(connection, `INSERT INTO ItemTable VALUES ${fill(keysValuesChunk.length / 2, '(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
|
||||
const keys: string[] = [];
|
||||
let length = 0;
|
||||
request.insert!.forEach((value, key) => {
|
||||
keys.push(key);
|
||||
length += value.length;
|
||||
});
|
||||
|
||||
return `Keys: ${keys.join(', ')} Length: ${length}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (request.delete && request.delete.size) {
|
||||
const keysChunks: (string[])[] = [];
|
||||
keysChunks.push([]); // seed with initial empty chunk
|
||||
|
||||
// Split keys into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
|
||||
// so that we can efficiently run the DELETE with as many HOST parameters
|
||||
// as possible
|
||||
let currentChunkIndex = 0;
|
||||
request.delete.forEach(key => {
|
||||
let keyChunk = keysChunks[currentChunkIndex];
|
||||
|
||||
if (keyChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
|
||||
currentChunkIndex++;
|
||||
keyChunk = [];
|
||||
keysChunks.push(keyChunk);
|
||||
}
|
||||
|
||||
keyChunk.push(key);
|
||||
});
|
||||
|
||||
keysChunks.forEach(keysChunk => {
|
||||
this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${fill(keysChunk.length, '?').join(',')})`, stmt => stmt.run(keysChunk), () => {
|
||||
const keys: string[] = [];
|
||||
request.delete!.forEach(key => {
|
||||
keys.push(key);
|
||||
});
|
||||
|
||||
return `Keys: ${keys.join(', ')}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
close(recovery?: () => Map<string, string>): Promise<void> {
|
||||
this.logger.trace(`[storage ${this.name}] close()`);
|
||||
|
||||
return this.whenOpened.then(result => {
|
||||
return new Promise((resolve, reject) => {
|
||||
result.db.close(error => {
|
||||
if (error) {
|
||||
this.logger.error(`[storage ${this.name}] close(): ${error}`);
|
||||
return this.whenConnected.then(connection => this.doClose(connection, recovery));
|
||||
}
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
// If the DB closed successfully and we are not running in-memory
|
||||
// make a backup of the DB so that we can use it as fallback in
|
||||
// case the actual DB becomes corrupt.
|
||||
if (result.path !== SQLiteStorageDatabase.IN_MEMORY_PATH) {
|
||||
return this.backup(result).then(resolve, error => {
|
||||
this.logger.error(`[storage ${this.name}] backup(): ${error}`);
|
||||
|
||||
return resolve(); // ignore failing backup
|
||||
});
|
||||
}
|
||||
private doClose(connection: IDatabaseConnection, recovery?: () => Map<string, string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.close(closeError => {
|
||||
if (closeError) {
|
||||
this.handleSQLiteError(connection, closeError, `[storage ${this.name}] close(): ${closeError}`);
|
||||
}
|
||||
|
||||
// Return early if this storage was created only in-memory
|
||||
// e.g. when running tests we do not need to backup.
|
||||
if (this.path === SQLiteStorageDatabase.IN_MEMORY_PATH) {
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// If the DB closed successfully and we are not running in-memory
|
||||
// and the DB did not get errors during runtime, make a backup
|
||||
// of the DB so that we can use it as fallback in case the actual
|
||||
// DB becomes corrupt in the future.
|
||||
if (!connection.isErroneous && !connection.isInMemory) {
|
||||
return this.backup().then(resolve, error => {
|
||||
this.logger.error(`[storage ${this.name}] backup(): ${error}`);
|
||||
|
||||
return resolve(); // ignore failing backup
|
||||
});
|
||||
}
|
||||
|
||||
// Recovery: if we detected errors while using the DB or we are using
|
||||
// an inmemory DB (as a fallback to not being able to open the DB initially)
|
||||
// and we have a recovery function provided, we recreate the DB with this
|
||||
// data to recover all known data without loss if possible.
|
||||
if (typeof recovery === 'function') {
|
||||
|
||||
// Delete the existing DB. If the path does not exist or fails to
|
||||
// be deleted, we do not try to recover anymore because we assume
|
||||
// that the path is no longer writeable for us.
|
||||
return unlink(this.path).then(() => {
|
||||
|
||||
// Re-open the DB fresh
|
||||
return this.doConnect(this.path).then(recoveryConnection => {
|
||||
const closeRecoveryConnection = () => {
|
||||
return this.doClose(recoveryConnection, undefined /* do not attempt to recover again */);
|
||||
};
|
||||
|
||||
// Store items
|
||||
return this.doUpdateItems(recoveryConnection, { insert: recovery() }).then(() => closeRecoveryConnection(), error => {
|
||||
|
||||
// In case of an error updating items, still ensure to close the connection
|
||||
// to prevent SQLITE_BUSY errors when the connection is restablished
|
||||
closeRecoveryConnection();
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}).then(resolve, reject);
|
||||
}
|
||||
|
||||
// Finally without recovery we just reject
|
||||
return reject(closeError || new Error('Database has errors or is in-memory without recovery option'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private backup(db: IOpenDatabaseResult): Promise<void> {
|
||||
if (db.path === SQLiteStorageDatabase.IN_MEMORY_PATH) {
|
||||
return Promise.resolve(); // no backups when running in-memory
|
||||
}
|
||||
private backup(): Promise<void> {
|
||||
const backupPath = this.toBackupPath(this.path);
|
||||
|
||||
const backupPath = this.toBackupPath(db.path);
|
||||
|
||||
return unlinkIgnoreError(backupPath).then(() => copy(db.path, backupPath));
|
||||
return copy(this.path, backupPath);
|
||||
}
|
||||
|
||||
private toBackupPath(path: string): string {
|
||||
@@ -440,73 +535,70 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`);
|
||||
|
||||
return this.whenOpened.then(({ db }) => {
|
||||
return this.get(db, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check').then(row => {
|
||||
return full ? row['integrity_check'] : row['quick_check'];
|
||||
return this.whenConnected.then(connection => {
|
||||
return this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check').then(row => {
|
||||
const integrity = full ? row['integrity_check'] : row['quick_check'];
|
||||
|
||||
if (connection.isErroneous) {
|
||||
return `${integrity} (last error: ${connection.lastError})`;
|
||||
}
|
||||
|
||||
if (connection.isInMemory) {
|
||||
return `${integrity} (in-memory!)`;
|
||||
}
|
||||
|
||||
return integrity;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private open(path: string): Promise<IOpenDatabaseResult> {
|
||||
this.logger.trace(`[storage ${this.name}] open()`);
|
||||
private connect(path: string, retryOnBusy: boolean = true): Promise<IDatabaseConnection> {
|
||||
this.logger.trace(`[storage ${this.name}] open(${path}, retryOnBusy: ${retryOnBusy})`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const fallbackToInMemoryDatabase = (error: Error) => {
|
||||
this.logger.error(`[storage ${this.name}] open(): Error (open DB): ${error}`);
|
||||
this.logger.error(`[storage ${this.name}] open(): Falling back to in-memory DB`);
|
||||
return this.doConnect(path).then(undefined, error => {
|
||||
this.logger.error(`[storage ${this.name}] open(): Unable to open DB due to ${error}`);
|
||||
|
||||
// In case of any error to open the DB, use an in-memory
|
||||
// DB so that we always have a valid DB to talk to.
|
||||
this.doOpen(SQLiteStorageDatabase.IN_MEMORY_PATH).then(resolve, reject);
|
||||
};
|
||||
// SQLITE_BUSY should only arise if another process is locking the same DB we want
|
||||
// to open at that time. This typically never happens because a DB connection is
|
||||
// limited per window. However, in the event of a window reload, it may be possible
|
||||
// that the previous connection was not properly closed while the new connection is
|
||||
// already established.
|
||||
//
|
||||
// In this case we simply wait for some time and retry once to establish the connection.
|
||||
//
|
||||
if (error.code === 'SQLITE_BUSY' && retryOnBusy) {
|
||||
return timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT).then(() => this.connect(path, false /* not another retry */));
|
||||
}
|
||||
|
||||
this.doOpen(path).then(resolve, error => {
|
||||
// Otherwise, best we can do is to recover from a backup if that exists, as such we
|
||||
// move the DB to a different filename and try to load from backup. If that fails,
|
||||
// a new empty DB is being created automatically.
|
||||
//
|
||||
// The final fallback is to use an in-memory DB which should only happen if the target
|
||||
// folder is really not writeable for us.
|
||||
//
|
||||
return unlink(path)
|
||||
.then(() => renameIgnoreError(this.toBackupPath(path), path))
|
||||
.then(() => this.doConnect(path))
|
||||
.then(undefined, error => {
|
||||
this.logger.error(`[storage ${this.name}] open(): Unable to use backup due to ${error}`);
|
||||
|
||||
// TODO@Ben check if this is still happening. This error code should only arise if
|
||||
// another process is locking the same DB we want to open at that time. This typically
|
||||
// never happens because a DB connection is limited per window. However, in the event
|
||||
// of a window reload, it may be possible that the previous connection was not properly
|
||||
// closed while the new connection is already established.
|
||||
if (error.code === 'SQLITE_BUSY') {
|
||||
return this.handleSQLiteBusy(path).then(resolve, fallbackToInMemoryDatabase);
|
||||
}
|
||||
|
||||
// This error code indicates that even though the DB file exists,
|
||||
// SQLite cannot open it and signals it is corrupt or not a DB.
|
||||
if (error.code === 'SQLITE_CORRUPT' || error.code === 'SQLITE_NOTADB') {
|
||||
return this.handleSQLiteCorrupt(path, error).then(resolve, fallbackToInMemoryDatabase);
|
||||
}
|
||||
|
||||
// Otherwise give up and fallback to in-memory DB
|
||||
return fallbackToInMemoryDatabase(error);
|
||||
});
|
||||
// In case of any error to open the DB, use an in-memory
|
||||
// DB so that we always have a valid DB to talk to.
|
||||
return this.doConnect(SQLiteStorageDatabase.IN_MEMORY_PATH);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private handleSQLiteBusy(path: string): Promise<IOpenDatabaseResult> {
|
||||
this.logger.error(`[storage ${this.name}] open(): Retrying after ${SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT}ms due to SQLITE_BUSY`);
|
||||
private handleSQLiteError(connection: IDatabaseConnection, error: Error & { code?: string }, msg: string): void {
|
||||
connection.isErroneous = true;
|
||||
connection.lastError = msg;
|
||||
|
||||
// Retry after some time if the DB is busy
|
||||
return timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT).then(() => this.doOpen(path));
|
||||
this.logger.error(msg);
|
||||
}
|
||||
|
||||
private handleSQLiteCorrupt(path: string, error: any): Promise<IOpenDatabaseResult> {
|
||||
this.logger.error(`[storage ${this.name}] open(): Unable to open DB due to ${error.code}`);
|
||||
private doConnect(path: string): Promise<IDatabaseConnection> {
|
||||
|
||||
// Move corrupt DB to a different filename and try to load from backup
|
||||
// If that fails, a new empty DB is being created automatically
|
||||
return rename(path, this.toCorruptPath(path))
|
||||
.then(() => renameIgnoreError(this.toBackupPath(path), path))
|
||||
.then(() => this.doOpen(path));
|
||||
}
|
||||
|
||||
private toCorruptPath(path: string): string {
|
||||
const randomSuffix = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 4);
|
||||
|
||||
return `${path}.${randomSuffix}.corrupt`;
|
||||
}
|
||||
|
||||
private doOpen(path: string): Promise<IOpenDatabaseResult> {
|
||||
// TODO@Ben clean up performance markers
|
||||
return new Promise((resolve, reject) => {
|
||||
let measureRequireDuration = false;
|
||||
@@ -521,45 +613,48 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
mark('didRequireSQLite');
|
||||
}
|
||||
|
||||
const db: Database = new (this.logger.isTracing ? sqlite3.verbose().Database : sqlite3.Database)(path, error => {
|
||||
if (error) {
|
||||
return db ? db.close(() => reject(error)) : reject(error);
|
||||
}
|
||||
const connection: IDatabaseConnection = {
|
||||
db: new (this.logger.isTracing ? sqlite3.verbose().Database : sqlite3.Database)(path, error => {
|
||||
if (error) {
|
||||
return connection.db ? connection.db.close(() => reject(error)) : reject(error);
|
||||
}
|
||||
|
||||
// The following exec() statement serves two purposes:
|
||||
// - create the DB if it does not exist yet
|
||||
// - validate that the DB is not corrupt (the open() call does not throw otherwise)
|
||||
mark('willSetupSQLiteSchema');
|
||||
this.exec(db, [
|
||||
'PRAGMA user_version = 1;',
|
||||
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)'
|
||||
].join('')).then(() => {
|
||||
mark('didSetupSQLiteSchema');
|
||||
// The following exec() statement serves two purposes:
|
||||
// - create the DB if it does not exist yet
|
||||
// - validate that the DB is not corrupt (the open() call does not throw otherwise)
|
||||
mark('willSetupSQLiteSchema');
|
||||
return this.exec(connection, [
|
||||
'PRAGMA user_version = 1;',
|
||||
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)'
|
||||
].join('')).then(() => {
|
||||
mark('didSetupSQLiteSchema');
|
||||
|
||||
return resolve({ path, db });
|
||||
}, error => {
|
||||
mark('didSetupSQLiteSchema');
|
||||
return resolve(connection);
|
||||
}, error => {
|
||||
mark('didSetupSQLiteSchema');
|
||||
|
||||
return db.close(() => reject(error));
|
||||
});
|
||||
});
|
||||
return connection.db.close(() => reject(error));
|
||||
});
|
||||
}),
|
||||
isInMemory: path === SQLiteStorageDatabase.IN_MEMORY_PATH
|
||||
};
|
||||
|
||||
// Errors
|
||||
db.on('error', error => this.logger.error(`[storage ${this.name}] Error (event): ${error}`));
|
||||
connection.db.on('error', error => this.handleSQLiteError(connection, error, `[storage ${this.name}] Error (event): ${error}`));
|
||||
|
||||
// Tracing
|
||||
if (this.logger.isTracing) {
|
||||
db.on('trace', sql => this.logger.trace(`[storage ${this.name}] Trace (event): ${sql}`));
|
||||
connection.db.on('trace', sql => this.logger.trace(`[storage ${this.name}] Trace (event): ${sql}`));
|
||||
}
|
||||
});
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
private exec(db: Database, sql: string): Promise<void> {
|
||||
private exec(connection: IDatabaseConnection, sql: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.exec(sql, error => {
|
||||
connection.db.exec(sql, error => {
|
||||
if (error) {
|
||||
this.logger.error(`[storage ${this.name}] exec(): ${error}`);
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] exec(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
@@ -569,11 +664,11 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
private get(db: Database, sql: string): Promise<object> {
|
||||
private get(connection: IDatabaseConnection, sql: string): Promise<object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, (error, row) => {
|
||||
connection.db.get(sql, (error, row) => {
|
||||
if (error) {
|
||||
this.logger.error(`[storage ${this.name}] get(): ${error}`);
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] get(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
@@ -583,11 +678,11 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
private all(db: Database, sql: string): Promise<{ key: string, value: string }[]> {
|
||||
private all(connection: IDatabaseConnection, sql: string): Promise<{ key: string, value: string }[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, (error, rows) => {
|
||||
connection.db.all(sql, (error, rows) => {
|
||||
if (error) {
|
||||
this.logger.error(`[storage ${this.name}] all(): ${error}`);
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] all(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
@@ -597,16 +692,16 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
private transaction(db: Database, transactions: () => void): Promise<void> {
|
||||
private transaction(connection: IDatabaseConnection, transactions: () => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
connection.db.serialize(() => {
|
||||
connection.db.run('BEGIN TRANSACTION');
|
||||
|
||||
transactions();
|
||||
|
||||
db.run('END TRANSACTION', error => {
|
||||
connection.db.run('END TRANSACTION', error => {
|
||||
if (error) {
|
||||
this.logger.error(`[storage ${this.name}] transaction(): ${error}`);
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] transaction(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
@@ -617,11 +712,11 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
private prepare(db: Database, sql: string, runCallback: (stmt: Statement) => void): void {
|
||||
const stmt = db.prepare(sql);
|
||||
private prepare(connection: IDatabaseConnection, sql: string, runCallback: (stmt: Statement) => void, errorDetails: () => string): void {
|
||||
const stmt = connection.db.prepare(sql);
|
||||
|
||||
const statementErrorListener = error => {
|
||||
this.logger.error(`[storage ${this.name}] prepare(): ${error} (${sql})`);
|
||||
this.handleSQLiteError(connection, error, `[storage ${this.name}] prepare(): ${error} (${sql}). Details: ${errorDetails()}`);
|
||||
};
|
||||
|
||||
stmt.on('error', statementErrorListener);
|
||||
@@ -675,11 +770,11 @@ export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
getItems(): Thenable<Map<string, string>> {
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return Promise.resolve(this.items);
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Thenable<void> {
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => this.items.set(key, value));
|
||||
}
|
||||
@@ -691,11 +786,11 @@ export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
close(): Thenable<void> {
|
||||
close(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Thenable<string> {
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return Promise.resolve('ok');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user