mirror of
https://github.com/coder/code-server.git
synced 2026-05-10 14:27:26 +02:00
Update to VS Code 1.52.1
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage';
|
||||
import { StorageScope, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
|
||||
@@ -16,15 +16,7 @@ import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
|
||||
|
||||
export class BrowserStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
export class BrowserStorageService extends AbstractStorageService {
|
||||
|
||||
private globalStorage: IStorage | undefined;
|
||||
private workspaceStorage: IStorage | undefined;
|
||||
@@ -40,6 +32,10 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */));
|
||||
private runWhenIdleDisposable: IDisposable | undefined = undefined;
|
||||
|
||||
get hasPendingUpdate(): boolean {
|
||||
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
@@ -66,13 +62,13 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
|
||||
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
|
||||
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
|
||||
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
|
||||
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
|
||||
|
||||
// Global Storage
|
||||
this.globalStorageFile = joinPath(stateRoot, 'global.json');
|
||||
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
|
||||
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
|
||||
|
||||
// Init both
|
||||
await Promise.all([
|
||||
@@ -122,11 +118,16 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
return this.getStorage(scope).getNumber(key, fallbackValue);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
|
||||
return this.getStorage(scope).set(key, value);
|
||||
=======
|
||||
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
|
||||
this.getStorage(scope).set(key, value);
|
||||
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): void {
|
||||
protected doRemove(key: string, scope: StorageScope): void {
|
||||
this.getStorage(scope).delete(key);
|
||||
}
|
||||
|
||||
@@ -149,6 +150,13 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
throw new Error('Migrating storage is currently unsupported in Web');
|
||||
}
|
||||
|
||||
protected async doFlush(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.getStorage(StorageScope.GLOBAL).whenFlushed(),
|
||||
this.getStorage(StorageScope.WORKSPACE).whenFlushed()
|
||||
]);
|
||||
}
|
||||
|
||||
private doFlushWhenIdle(): void {
|
||||
|
||||
// Dispose any previous idle runner
|
||||
@@ -175,14 +183,6 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
});
|
||||
}
|
||||
|
||||
get hasPendingUpdate(): boolean {
|
||||
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// We explicitly do not close our DBs because writing data onBeforeUnload()
|
||||
// can result in unexpected results. Namely, it seems that - even though this
|
||||
@@ -195,10 +195,6 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope): boolean {
|
||||
return this.getBoolean(IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
@@ -4,18 +4,27 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Event, Emitter, PauseableEmitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
||||
export const IS_NEW_KEY = '__$__isNewStorageMarker';
|
||||
const TARGET_KEY = '__$__targetStorageMarker';
|
||||
|
||||
export const IStorageService = createDecorator<IStorageService>('storageService');
|
||||
|
||||
export enum WillSaveStateReason {
|
||||
NONE = 0,
|
||||
SHUTDOWN = 1
|
||||
|
||||
/**
|
||||
* No specific reason to save state.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* A hint that the workbench is about to shutdown.
|
||||
*/
|
||||
SHUTDOWN
|
||||
}
|
||||
|
||||
export interface IWillSaveStateEvent {
|
||||
@@ -29,7 +38,12 @@ export interface IStorageService {
|
||||
/**
|
||||
* Emitted whenever data is updated or deleted.
|
||||
*/
|
||||
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent>;
|
||||
readonly onDidChangeValue: Event<IStorageValueChangeEvent>;
|
||||
|
||||
/**
|
||||
* Emitted whenever target of a storage entry changes.
|
||||
*/
|
||||
readonly onDidChangeTarget: Event<IStorageTargetChangeEvent>;
|
||||
|
||||
/**
|
||||
* Emitted when the storage is about to persist. This is the right time
|
||||
@@ -48,44 +62,53 @@ export interface IStorageService {
|
||||
|
||||
/**
|
||||
* Retrieve an element stored with the given key from storage. Use
|
||||
* the provided defaultValue if the element is null or undefined.
|
||||
* the provided `defaultValue` if the element is `null` or `undefined`.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
* @param scope allows to define the scope of the storage operation
|
||||
* to either the current workspace only or all workspaces.
|
||||
*/
|
||||
get(key: string, scope: StorageScope, fallbackValue: string): string;
|
||||
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
|
||||
|
||||
/**
|
||||
* Retrieve an element stored with the given key from storage. Use
|
||||
* the provided defaultValue if the element is null or undefined. The element
|
||||
* will be converted to a boolean.
|
||||
* the provided `defaultValue` if the element is `null` or `undefined`.
|
||||
* The element will be converted to a `boolean`.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
* @param scope allows to define the scope of the storage operation
|
||||
* to either the current workspace only or all workspaces.
|
||||
*/
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
|
||||
|
||||
/**
|
||||
* Retrieve an element stored with the given key from storage. Use
|
||||
* the provided defaultValue if the element is null or undefined. The element
|
||||
* will be converted to a number using parseInt with a base of 10.
|
||||
* the provided `defaultValue` if the element is `null` or `undefined`.
|
||||
* The element will be converted to a `number` using `parseInt` with a
|
||||
* base of `10`.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
* @param scope allows to define the scope of the storage operation
|
||||
* to either the current workspace only or all workspaces.
|
||||
*/
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
|
||||
|
||||
/**
|
||||
* Store a value under the given key to storage. The value will be converted to a string.
|
||||
* Storing either undefined or null will remove the entry under the key.
|
||||
* Store a value under the given key to storage. The value will be
|
||||
* converted to a `string`. Storing either `undefined` or `null` will
|
||||
* remove the entry under the key.
|
||||
*
|
||||
* The scope argument allows to define the scope of the storage
|
||||
* operation to either the current workspace only or all workspaces.
|
||||
* @param scope allows to define the scope of the storage operation
|
||||
* to either the current workspace only or all workspaces.
|
||||
*
|
||||
* @param target allows to define the target of the storage operation
|
||||
* to either the current machine or user.
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> | void;
|
||||
=======
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void;
|
||||
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
|
||||
|
||||
/**
|
||||
* Delete an element stored under the provided key from storage.
|
||||
@@ -95,6 +118,22 @@ export interface IStorageService {
|
||||
*/
|
||||
remove(key: string, scope: StorageScope): void;
|
||||
|
||||
/**
|
||||
* Returns all the keys used in the storage for the provided `scope`
|
||||
* and `target`.
|
||||
*
|
||||
* Note: this will NOT return all keys stored in the storage layer.
|
||||
* Some keys may not have an associated `StorageTarget` and thus
|
||||
* will be excluded from the results.
|
||||
*
|
||||
* @param scope allows to define the scope for the keys
|
||||
* to either the current workspace only or all workspaces.
|
||||
*
|
||||
* @param target allows to define the target for the keys
|
||||
* to either the current machine or user.
|
||||
*/
|
||||
keys(scope: StorageScope, target: StorageTarget): string[];
|
||||
|
||||
/**
|
||||
* Log the contents of the storage to the console.
|
||||
*/
|
||||
@@ -108,16 +147,18 @@ export interface IStorageService {
|
||||
/**
|
||||
* Whether the storage for the given scope was created during this session or
|
||||
* existed before.
|
||||
*
|
||||
*/
|
||||
isNew(scope: StorageScope): boolean;
|
||||
|
||||
/**
|
||||
* Allows to flush state, e.g. in cases where a shutdown is
|
||||
* imminent. This will send out the onWillSaveState to ask
|
||||
* imminent. This will send out the `onWillSaveState` to ask
|
||||
* everyone for latest state.
|
||||
*
|
||||
* @returns a `Promise` that can be awaited on when all updates
|
||||
* to the underlying storage have been flushed.
|
||||
*/
|
||||
flush(): void;
|
||||
flush(): Promise<void>;
|
||||
}
|
||||
|
||||
export const enum StorageScope {
|
||||
@@ -133,21 +174,247 @@ export const enum StorageScope {
|
||||
WORKSPACE
|
||||
}
|
||||
|
||||
export interface IWorkspaceStorageChangeEvent {
|
||||
export const enum StorageTarget {
|
||||
|
||||
/**
|
||||
* The stored data is user specific and applies across machines.
|
||||
*/
|
||||
USER,
|
||||
|
||||
/**
|
||||
* The stored data is machine specific.
|
||||
*/
|
||||
MACHINE
|
||||
}
|
||||
|
||||
export interface IStorageValueChangeEvent {
|
||||
|
||||
/**
|
||||
* The scope for the storage entry that changed
|
||||
* or was removed.
|
||||
*/
|
||||
readonly scope: StorageScope;
|
||||
|
||||
/**
|
||||
* The `key` of the storage entry that was changed
|
||||
* or was removed.
|
||||
*/
|
||||
readonly key: string;
|
||||
|
||||
/**
|
||||
* The `target` can be `undefined` if a key is being
|
||||
* removed.
|
||||
*/
|
||||
readonly target: StorageTarget | undefined;
|
||||
}
|
||||
|
||||
export interface IStorageTargetChangeEvent {
|
||||
|
||||
/**
|
||||
* The scope for the target that changed. Listeners
|
||||
* should use `keys(scope, target)` to get an updated
|
||||
* list of keys for the given `scope` and `target`.
|
||||
*/
|
||||
readonly scope: StorageScope;
|
||||
}
|
||||
|
||||
export class InMemoryStorageService extends Disposable implements IStorageService {
|
||||
interface IKeyTargets {
|
||||
[key: string]: StorageTarget
|
||||
}
|
||||
|
||||
export abstract class AbstractStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
private readonly _onDidChangeValue = this._register(new PauseableEmitter<IStorageValueChangeEvent>());
|
||||
readonly onDidChangeValue = this._onDidChangeValue.event;
|
||||
|
||||
protected readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
private readonly _onDidChangeTarget = this._register(new PauseableEmitter<IStorageTargetChangeEvent>());
|
||||
readonly onDidChangeTarget = this._onDidChangeTarget.event;
|
||||
|
||||
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
|
||||
protected emitDidChangeValue(scope: StorageScope, key: string): void {
|
||||
|
||||
// Specially handle `TARGET_KEY`
|
||||
if (key === TARGET_KEY) {
|
||||
|
||||
// Clear our cached version which is now out of date
|
||||
if (scope === StorageScope.GLOBAL) {
|
||||
this._globalKeyTargets = undefined;
|
||||
} else if (scope === StorageScope.WORKSPACE) {
|
||||
this._workspaceKeyTargets = undefined;
|
||||
}
|
||||
|
||||
// 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] });
|
||||
}
|
||||
}
|
||||
|
||||
protected emitWillSaveState(reason: WillSaveStateReason): void {
|
||||
this._onWillSaveState.fire({ reason });
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void {
|
||||
|
||||
// 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.doStore(key, value, scope);
|
||||
});
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): void {
|
||||
|
||||
// Update our datastructures but send events only after
|
||||
this.withPausedEmitters(() => {
|
||||
|
||||
// Update key-target map
|
||||
this.updateKeyTarget(key, scope, undefined);
|
||||
|
||||
// Remove actual key
|
||||
this.doRemove(key, scope);
|
||||
});
|
||||
}
|
||||
|
||||
private withPausedEmitters(fn: Function): void {
|
||||
|
||||
// Pause emitters
|
||||
this._onDidChangeValue.pause();
|
||||
this._onDidChangeTarget.pause();
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
|
||||
// Resume emitters
|
||||
this._onDidChangeValue.resume();
|
||||
this._onDidChangeTarget.resume();
|
||||
}
|
||||
}
|
||||
|
||||
keys(scope: StorageScope, target: StorageTarget): string[] {
|
||||
const keys: string[] = [];
|
||||
|
||||
const keyTargets = this.getKeyTargets(scope);
|
||||
for (const key of Object.keys(keyTargets)) {
|
||||
const keyTarget = keyTargets[key];
|
||||
if (keyTarget === target) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
private updateKeyTarget(key: string, scope: StorageScope, target: StorageTarget | undefined): void {
|
||||
|
||||
// Add
|
||||
const keyTargets = this.getKeyTargets(scope);
|
||||
if (typeof target === 'number') {
|
||||
if (keyTargets[key] !== target) {
|
||||
keyTargets[key] = target;
|
||||
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove
|
||||
else {
|
||||
if (typeof keyTargets[key] === 'number') {
|
||||
delete keyTargets[key];
|
||||
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _workspaceKeyTargets: IKeyTargets | undefined = undefined;
|
||||
private get workspaceKeyTargets(): IKeyTargets {
|
||||
if (!this._workspaceKeyTargets) {
|
||||
this._workspaceKeyTargets = this.loadKeyTargets(StorageScope.WORKSPACE);
|
||||
}
|
||||
|
||||
return this._workspaceKeyTargets;
|
||||
}
|
||||
|
||||
private _globalKeyTargets: IKeyTargets | undefined = undefined;
|
||||
private get globalKeyTargets(): IKeyTargets {
|
||||
if (!this._globalKeyTargets) {
|
||||
this._globalKeyTargets = this.loadKeyTargets(StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
return this._globalKeyTargets;
|
||||
}
|
||||
|
||||
private getKeyTargets(scope: StorageScope): IKeyTargets {
|
||||
return scope === StorageScope.GLOBAL ? this.globalKeyTargets : this.workspaceKeyTargets;
|
||||
}
|
||||
|
||||
private loadKeyTargets(scope: StorageScope): { [key: string]: StorageTarget } {
|
||||
const keysRaw = this.get(TARGET_KEY, scope);
|
||||
if (keysRaw) {
|
||||
try {
|
||||
return JSON.parse(keysRaw);
|
||||
} catch (error) {
|
||||
// Fail gracefully
|
||||
}
|
||||
}
|
||||
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope): boolean {
|
||||
return this.getBoolean(IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
|
||||
flush(): Promise<void> {
|
||||
|
||||
// Signal event to collect changes
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
|
||||
// Await flush
|
||||
return this.doFlush();
|
||||
}
|
||||
|
||||
// --- abstract
|
||||
|
||||
abstract get(key: string, scope: StorageScope, fallbackValue: string): string;
|
||||
abstract get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
|
||||
|
||||
abstract getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
|
||||
abstract getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
|
||||
|
||||
abstract getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
|
||||
abstract getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
|
||||
|
||||
protected abstract doStore(key: string, value: string | boolean | number, scope: StorageScope): void;
|
||||
|
||||
protected abstract doRemove(key: string, scope: StorageScope): void;
|
||||
|
||||
protected abstract doFlush(): Promise<void>;
|
||||
|
||||
abstract migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
|
||||
|
||||
abstract logStorage(): void;
|
||||
}
|
||||
|
||||
export class InMemoryStorageService extends AbstractStorageService {
|
||||
|
||||
private readonly globalCache = new Map<string, string>();
|
||||
private readonly workspaceCache = new Map<string, string>();
|
||||
|
||||
@@ -188,12 +455,7 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.remove(key, scope);
|
||||
}
|
||||
protected doStore(key: string, value: string | boolean | number, scope: StorageScope): void {
|
||||
|
||||
// Otherwise, convert to String and store
|
||||
const valueStr = String(value);
|
||||
@@ -201,28 +463,24 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
|
||||
// Return early if value already set
|
||||
const currentValue = this.getCache(scope).get(key);
|
||||
if (currentValue === valueStr) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update in cache
|
||||
this.getCache(scope).set(key, valueStr);
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
this.emitDidChangeValue(scope, key);
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): Promise<void> {
|
||||
protected doRemove(key: string, scope: StorageScope): void {
|
||||
const wasDeleted = this.getCache(scope).delete(key);
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
return; // Return early if value already deleted
|
||||
}
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
this.emitDidChangeValue(scope, key);
|
||||
}
|
||||
|
||||
logStorage(): void {
|
||||
@@ -233,13 +491,7 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
|
||||
// not supported
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
isNew(): boolean {
|
||||
return true; // always new when in-memory
|
||||
}
|
||||
async doFlush(): Promise<void> { }
|
||||
|
||||
async close(): Promise<void> { }
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage';
|
||||
import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
|
||||
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
@@ -17,19 +16,11 @@ import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderW
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
|
||||
|
||||
export class NativeStorageService extends Disposable implements IStorageService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
export class NativeStorageService extends AbstractStorageService {
|
||||
|
||||
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
|
||||
private static readonly WORKSPACE_META_NAME = 'workspace.json';
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
readonly onWillSaveState = this._onWillSaveState.event;
|
||||
|
||||
private readonly globalStorage = new Storage(this.globalStorageDatabase);
|
||||
|
||||
private workspaceStoragePath: string | undefined;
|
||||
@@ -54,11 +45,7 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
private registerListeners(): void {
|
||||
|
||||
// Global Storage change events
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.GLOBAL)));
|
||||
}
|
||||
|
||||
private handleDidChangeStorage(key: string, scope: StorageScope): void {
|
||||
this._onDidChangeStorage.fire({ key, scope });
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
|
||||
}
|
||||
|
||||
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
|
||||
@@ -134,7 +121,7 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
// Create new
|
||||
this.workspaceStoragePath = workspaceStoragePath;
|
||||
this.workspaceStorage = new Storage(new SQLiteStorageDatabase(workspaceStoragePath, { logging: workspaceLoggingOptions }), { hint });
|
||||
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.WORKSPACE));
|
||||
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key));
|
||||
|
||||
return this.workspaceStorage;
|
||||
}
|
||||
@@ -201,11 +188,16 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
return this.getStorage(scope).getNumber(key, fallbackValue);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
|
||||
return this.getStorage(scope).set(key, value);
|
||||
=======
|
||||
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
|
||||
this.getStorage(scope).set(key, value);
|
||||
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): void {
|
||||
protected doRemove(key: string, scope: StorageScope): void {
|
||||
this.getStorage(scope).delete(key);
|
||||
}
|
||||
|
||||
@@ -213,6 +205,19 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
|
||||
}
|
||||
|
||||
protected async doFlush(): Promise<void> {
|
||||
const promises: Promise<unknown>[] = [];
|
||||
if (this.globalStorage) {
|
||||
promises.push(this.globalStorage.whenFlushed());
|
||||
}
|
||||
|
||||
if (this.workspaceStorage) {
|
||||
promises.push(this.workspaceStorage.whenFlushed());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private doFlushWhenIdle(): void {
|
||||
|
||||
// Dispose any previous idle runner
|
||||
@@ -229,10 +234,6 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
});
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
||||
// Stop periodic scheduler and idle runner as we now collect state normally
|
||||
@@ -241,7 +242,7 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
// Signal as event so that clients can still store data
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
|
||||
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
|
||||
|
||||
// Do it
|
||||
await Promise.all([
|
||||
@@ -277,8 +278,4 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
// Recreate and init workspace storage
|
||||
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope): boolean {
|
||||
return this.getBoolean(IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { strictEqual, ok, equal } from 'assert';
|
||||
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage';
|
||||
|
||||
suite('StorageService', function () {
|
||||
|
||||
test('Get Data, Integer, Boolean (global, in-memory)', () => {
|
||||
storeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
|
||||
storeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function storeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
|
||||
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
|
||||
|
||||
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
|
||||
strictEqual(storage.get('test.get', scope, ''), '');
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
|
||||
|
||||
storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
|
||||
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
|
||||
equal(storageValueChangeEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.key, 'test.get');
|
||||
storageValueChangeEvents = [];
|
||||
|
||||
storage.store('test.get', '', scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), '');
|
||||
storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
|
||||
equal(storageValueChangeEvent!.scope, scope);
|
||||
equal(storageValueChangeEvent!.key, 'test.get');
|
||||
|
||||
storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
|
||||
|
||||
storage.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
|
||||
|
||||
storage.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
|
||||
|
||||
storage.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
|
||||
|
||||
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
|
||||
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
|
||||
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
|
||||
}
|
||||
|
||||
test('Remove Data (global, in-memory)', () => {
|
||||
removeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Remove Data (workspace, in-memory)', () => {
|
||||
removeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function removeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
|
||||
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
|
||||
|
||||
storage.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
|
||||
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
|
||||
|
||||
storage.remove('test.remove', scope);
|
||||
ok(!storage.get('test.remove', scope, (undefined)!));
|
||||
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove');
|
||||
equal(storageValueChangeEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.key, 'test.remove');
|
||||
}
|
||||
|
||||
test('Keys (in-memory)', () => {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined;
|
||||
storage.onDidChangeTarget(e => storageTargetEvent = e);
|
||||
|
||||
let storageValueChangeEvent: IStorageValueChangeEvent | undefined = undefined;
|
||||
storage.onDidChangeValue(e => storageValueChangeEvent = e);
|
||||
|
||||
// Empty
|
||||
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
|
||||
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
|
||||
strictEqual(storage.keys(scope, target).length, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Add values
|
||||
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
|
||||
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
|
||||
storageTargetEvent = Object.create(null);
|
||||
storageValueChangeEvent = Object.create(null);
|
||||
|
||||
storage.store('test.target1', 'value1', scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 1);
|
||||
equal(storageTargetEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.key, 'test.target1');
|
||||
equal(storageValueChangeEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.target, target);
|
||||
|
||||
storageTargetEvent = undefined;
|
||||
storageValueChangeEvent = Object.create(null);
|
||||
|
||||
storage.store('test.target1', 'otherValue1', scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 1);
|
||||
equal(storageTargetEvent, undefined);
|
||||
equal(storageValueChangeEvent?.key, 'test.target1');
|
||||
equal(storageValueChangeEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.target, target);
|
||||
|
||||
storage.store('test.target2', 'value2', scope, target);
|
||||
storage.store('test.target3', 'value3', scope, target);
|
||||
|
||||
strictEqual(storage.keys(scope, target).length, 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove values
|
||||
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
|
||||
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
|
||||
const keysLength = storage.keys(scope, target).length;
|
||||
|
||||
storage.store('test.target4', 'value1', scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, keysLength + 1);
|
||||
|
||||
storageTargetEvent = Object.create(null);
|
||||
storageValueChangeEvent = Object.create(null);
|
||||
|
||||
storage.remove('test.target4', scope);
|
||||
strictEqual(storage.keys(scope, target).length, keysLength);
|
||||
equal(storageTargetEvent?.scope, scope);
|
||||
equal(storageValueChangeEvent?.key, 'test.target4');
|
||||
equal(storageValueChangeEvent?.scope, scope);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all
|
||||
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
|
||||
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
|
||||
const keys = storage.keys(scope, target);
|
||||
|
||||
for (const key of keys) {
|
||||
storage.remove(key, scope);
|
||||
}
|
||||
|
||||
strictEqual(storage.keys(scope, target).length, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Adding undefined or null removes value
|
||||
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
|
||||
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
|
||||
storage.store('test.target1', 'value1', scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 1);
|
||||
|
||||
storageTargetEvent = Object.create(null);
|
||||
|
||||
storage.store('test.target1', undefined, scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 0);
|
||||
equal(storageTargetEvent?.scope, scope);
|
||||
|
||||
storage.store('test.target1', '', scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 1);
|
||||
|
||||
storage.store('test.target1', null, scope, target);
|
||||
strictEqual(storage.keys(scope, target).length, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Target change
|
||||
storageTargetEvent = undefined;
|
||||
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
ok(storageTargetEvent);
|
||||
storageTargetEvent = undefined;
|
||||
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
|
||||
ok(storageTargetEvent);
|
||||
storageTargetEvent = undefined;
|
||||
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
ok(storageTargetEvent);
|
||||
storageTargetEvent = undefined;
|
||||
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
ok(!storageTargetEvent); // no change in target
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { strictEqual, ok, equal } from 'assert';
|
||||
import { StorageScope, InMemoryStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { equal } from 'assert';
|
||||
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join } from 'vs/base/common/path';
|
||||
@@ -16,66 +16,7 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
|
||||
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('StorageService', function () {
|
||||
|
||||
test('Remove Data (global, in-memory)', () => {
|
||||
removeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Remove Data (workspace, in-memory)', () => {
|
||||
removeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function removeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
storage.store('test.remove', 'foobar', scope);
|
||||
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
|
||||
|
||||
storage.remove('test.remove', scope);
|
||||
ok(!storage.get('test.remove', scope, (undefined)!));
|
||||
}
|
||||
|
||||
test('Get Data, Integer, Boolean (global, in-memory)', () => {
|
||||
storeData(StorageScope.GLOBAL);
|
||||
});
|
||||
|
||||
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
|
||||
storeData(StorageScope.WORKSPACE);
|
||||
});
|
||||
|
||||
function storeData(scope: StorageScope): void {
|
||||
const storage = new InMemoryStorageService();
|
||||
|
||||
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
|
||||
strictEqual(storage.get('test.get', scope, ''), '');
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
|
||||
|
||||
storage.store('test.get', 'foobar', scope);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
|
||||
|
||||
storage.store('test.get', '', scope);
|
||||
strictEqual(storage.get('test.get', scope, (undefined)!), '');
|
||||
|
||||
storage.store('test.getNumber', 5, scope);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
|
||||
|
||||
storage.store('test.getNumber', 0, scope);
|
||||
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
|
||||
|
||||
storage.store('test.getBoolean', true, scope);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
|
||||
|
||||
storage.store('test.getBoolean', false, scope);
|
||||
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
|
||||
|
||||
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
|
||||
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
|
||||
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
|
||||
}
|
||||
suite('NativeStorageService', function () {
|
||||
|
||||
function uniqueStorageDir(): string {
|
||||
const id = generateUuid();
|
||||
@@ -111,9 +52,13 @@ suite('StorageService', function () {
|
||||
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir));
|
||||
await storage.initialize({ id: String(Date.now()) });
|
||||
|
||||
storage.store('bar', 'foo', StorageScope.WORKSPACE);
|
||||
storage.store('barNumber', 55, StorageScope.WORKSPACE);
|
||||
storage.store('barBoolean', true, StorageScope.GLOBAL);
|
||||
storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
|
||||
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
|
||||
equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
|
||||
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
|
||||
|
||||
await storage.migrate({ id: String(Date.now() + 100) });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user