Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/parts/storage/common/storage.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ThrottledDelayer } from '../../../common/async.js';
7
import { Event, PauseableEmitter } from '../../../common/event.js';
8
import { Disposable, IDisposable } from '../../../common/lifecycle.js';
9
import { parse, stringify } from '../../../common/marshalling.js';
10
import { isObject, isUndefinedOrNull } from '../../../common/types.js';
11
12
export enum StorageHint {
13
14
// A hint to the storage that the storage
15
// does not exist on disk yet. This allows
16
// the storage library to improve startup
17
// time by not checking the storage for data.
18
STORAGE_DOES_NOT_EXIST,
19
20
// A hint to the storage that the storage
21
// is backed by an in-memory storage.
22
STORAGE_IN_MEMORY
23
}
24
25
export interface IStorageOptions {
26
readonly hint?: StorageHint;
27
}
28
29
export interface IUpdateRequest {
30
readonly insert?: Map<string, string>;
31
readonly delete?: Set<string>;
32
}
33
34
export interface IStorageItemsChangeEvent {
35
readonly changed?: Map<string, string>;
36
readonly deleted?: Set<string>;
37
}
38
39
export function isStorageItemsChangeEvent(thing: unknown): thing is IStorageItemsChangeEvent {
40
const candidate = thing as IStorageItemsChangeEvent | undefined;
41
42
return candidate?.changed instanceof Map || candidate?.deleted instanceof Set;
43
}
44
45
export interface IStorageDatabase {
46
47
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
48
49
getItems(): Promise<Map<string, string>>;
50
updateItems(request: IUpdateRequest): Promise<void>;
51
52
optimize(): Promise<void>;
53
54
close(recovery?: () => Map<string, string>): Promise<void>;
55
}
56
57
export interface IStorageChangeEvent {
58
59
/**
60
* The `key` of the storage entry that was changed
61
* or was removed.
62
*/
63
readonly key: string;
64
65
/**
66
* A hint how the storage change event was triggered. If
67
* `true`, the storage change was triggered by an external
68
* source, such as:
69
* - another process (for example another window)
70
* - operations such as settings sync or profiles change
71
*/
72
readonly external?: boolean;
73
}
74
75
export type StorageValue = string | boolean | number | undefined | null | object;
76
77
export interface IStorage extends IDisposable {
78
79
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
80
81
readonly items: Map<string, string>;
82
readonly size: number;
83
84
init(): Promise<void>;
85
86
get(key: string, fallbackValue: string): string;
87
get(key: string, fallbackValue?: string): string | undefined;
88
89
getBoolean(key: string, fallbackValue: boolean): boolean;
90
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
91
92
getNumber(key: string, fallbackValue: number): number;
93
getNumber(key: string, fallbackValue?: number): number | undefined;
94
95
getObject<T extends object>(key: string, fallbackValue: T): T;
96
getObject<T extends object>(key: string, fallbackValue?: T): T | undefined;
97
98
set(key: string, value: StorageValue, external?: boolean): Promise<void>;
99
delete(key: string, external?: boolean): Promise<void>;
100
101
flush(delay?: number): Promise<void>;
102
whenFlushed(): Promise<void>;
103
104
optimize(): Promise<void>;
105
106
close(): Promise<void>;
107
}
108
109
export enum StorageState {
110
None,
111
Initialized,
112
Closed
113
}
114
115
export class Storage extends Disposable implements IStorage {
116
117
private static readonly DEFAULT_FLUSH_DELAY = 100;
118
119
private readonly _onDidChangeStorage = this._register(new PauseableEmitter<IStorageChangeEvent>());
120
readonly onDidChangeStorage = this._onDidChangeStorage.event;
121
122
private state = StorageState.None;
123
124
private cache = new Map<string, string>();
125
126
private readonly flushDelayer = this._register(new ThrottledDelayer<void>(Storage.DEFAULT_FLUSH_DELAY));
127
128
private pendingDeletes = new Set<string>();
129
private pendingInserts = new Map<string, string>();
130
131
private pendingClose: Promise<void> | undefined = undefined;
132
133
private readonly whenFlushedCallbacks: Function[] = [];
134
135
constructor(
136
protected readonly database: IStorageDatabase,
137
private readonly options: IStorageOptions = Object.create(null)
138
) {
139
super();
140
141
this.registerListeners();
142
}
143
144
private registerListeners(): void {
145
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
146
}
147
148
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
149
this._onDidChangeStorage.pause();
150
151
try {
152
// items that change external require us to update our
153
// caches with the values. we just accept the value and
154
// emit an event if there is a change.
155
156
e.changed?.forEach((value, key) => this.acceptExternal(key, value));
157
e.deleted?.forEach(key => this.acceptExternal(key, undefined));
158
159
} finally {
160
this._onDidChangeStorage.resume();
161
}
162
}
163
164
private acceptExternal(key: string, value: string | undefined): void {
165
if (this.state === StorageState.Closed) {
166
return; // Return early if we are already closed
167
}
168
169
let changed = false;
170
171
// Item got removed, check for deletion
172
if (isUndefinedOrNull(value)) {
173
changed = this.cache.delete(key);
174
}
175
176
// Item got updated, check for change
177
else {
178
const currentValue = this.cache.get(key);
179
if (currentValue !== value) {
180
this.cache.set(key, value);
181
changed = true;
182
}
183
}
184
185
// Signal to outside listeners
186
if (changed) {
187
this._onDidChangeStorage.fire({ key, external: true });
188
}
189
}
190
191
get items(): Map<string, string> {
192
return this.cache;
193
}
194
195
get size(): number {
196
return this.cache.size;
197
}
198
199
async init(): Promise<void> {
200
if (this.state !== StorageState.None) {
201
return; // either closed or already initialized
202
}
203
204
this.state = StorageState.Initialized;
205
206
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
207
// return early if we know the storage file does not exist. this is a performance
208
// optimization to not load all items of the underlying storage if we know that
209
// there can be no items because the storage does not exist.
210
return;
211
}
212
213
this.cache = await this.database.getItems();
214
}
215
216
get(key: string, fallbackValue: string): string;
217
get(key: string, fallbackValue?: string): string | undefined;
218
get(key: string, fallbackValue?: string): string | undefined {
219
const value = this.cache.get(key);
220
221
if (isUndefinedOrNull(value)) {
222
return fallbackValue;
223
}
224
225
return value;
226
}
227
228
getBoolean(key: string, fallbackValue: boolean): boolean;
229
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
230
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
231
const value = this.get(key);
232
233
if (isUndefinedOrNull(value)) {
234
return fallbackValue;
235
}
236
237
return value === 'true';
238
}
239
240
getNumber(key: string, fallbackValue: number): number;
241
getNumber(key: string, fallbackValue?: number): number | undefined;
242
getNumber(key: string, fallbackValue?: number): number | undefined {
243
const value = this.get(key);
244
245
if (isUndefinedOrNull(value)) {
246
return fallbackValue;
247
}
248
249
return parseInt(value, 10);
250
}
251
252
getObject(key: string, fallbackValue: object): object;
253
getObject(key: string, fallbackValue?: object | undefined): object | undefined;
254
getObject(key: string, fallbackValue?: object): object | undefined {
255
const value = this.get(key);
256
257
if (isUndefinedOrNull(value)) {
258
return fallbackValue;
259
}
260
261
return parse(value);
262
}
263
264
async set(key: string, value: string | boolean | number | null | undefined | object, external = false): Promise<void> {
265
if (this.state === StorageState.Closed) {
266
return; // Return early if we are already closed
267
}
268
269
// We remove the key for undefined/null values
270
if (isUndefinedOrNull(value)) {
271
return this.delete(key, external);
272
}
273
274
// Otherwise, convert to String and store
275
const valueStr = isObject(value) || Array.isArray(value) ? stringify(value) : String(value);
276
277
// Return early if value already set
278
const currentValue = this.cache.get(key);
279
if (currentValue === valueStr) {
280
return;
281
}
282
283
// Update in cache and pending
284
this.cache.set(key, valueStr);
285
this.pendingInserts.set(key, valueStr);
286
this.pendingDeletes.delete(key);
287
288
// Event
289
this._onDidChangeStorage.fire({ key, external });
290
291
// Accumulate work by scheduling after timeout
292
return this.doFlush();
293
}
294
295
async delete(key: string, external = false): Promise<void> {
296
if (this.state === StorageState.Closed) {
297
return; // Return early if we are already closed
298
}
299
300
// Remove from cache and add to pending
301
const wasDeleted = this.cache.delete(key);
302
if (!wasDeleted) {
303
return; // Return early if value already deleted
304
}
305
306
if (!this.pendingDeletes.has(key)) {
307
this.pendingDeletes.add(key);
308
}
309
310
this.pendingInserts.delete(key);
311
312
// Event
313
this._onDidChangeStorage.fire({ key, external });
314
315
// Accumulate work by scheduling after timeout
316
return this.doFlush();
317
}
318
319
async optimize(): Promise<void> {
320
if (this.state === StorageState.Closed) {
321
return; // Return early if we are already closed
322
}
323
324
// Await pending data to be flushed to the DB
325
// before attempting to optimize the DB
326
await this.flush(0);
327
328
return this.database.optimize();
329
}
330
331
async close(): Promise<void> {
332
if (!this.pendingClose) {
333
this.pendingClose = this.doClose();
334
}
335
336
return this.pendingClose;
337
}
338
339
private async doClose(): Promise<void> {
340
341
// Update state
342
this.state = StorageState.Closed;
343
344
// Trigger new flush to ensure data is persisted and then close
345
// even if there is an error flushing. We must always ensure
346
// the DB is closed to avoid corruption.
347
//
348
// Recovery: we pass our cache over as recovery option in case
349
// the DB is not healthy.
350
try {
351
await this.doFlush(0 /* as soon as possible */);
352
} catch (error) {
353
// Ignore
354
}
355
356
await this.database.close(() => this.cache);
357
}
358
359
private get hasPending() {
360
return this.pendingInserts.size > 0 || this.pendingDeletes.size > 0;
361
}
362
363
private async flushPending(): Promise<void> {
364
if (!this.hasPending) {
365
return; // return early if nothing to do
366
}
367
368
// Get pending data
369
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
370
371
// Reset pending data for next run
372
this.pendingDeletes = new Set<string>();
373
this.pendingInserts = new Map<string, string>();
374
375
// Update in storage and release any
376
// waiters we have once done
377
return this.database.updateItems(updateRequest).finally(() => {
378
if (!this.hasPending) {
379
while (this.whenFlushedCallbacks.length) {
380
this.whenFlushedCallbacks.pop()?.();
381
}
382
}
383
});
384
}
385
386
async flush(delay?: number): Promise<void> {
387
if (
388
this.state === StorageState.Closed || // Return early if we are already closed
389
this.pendingClose // return early if nothing to do
390
) {
391
return;
392
}
393
394
return this.doFlush(delay);
395
}
396
397
private async doFlush(delay?: number): Promise<void> {
398
if (this.options.hint === StorageHint.STORAGE_IN_MEMORY) {
399
return this.flushPending(); // return early if in-memory
400
}
401
402
return this.flushDelayer.trigger(() => this.flushPending(), delay);
403
}
404
405
async whenFlushed(): Promise<void> {
406
if (!this.hasPending) {
407
return; // return early if nothing to do
408
}
409
410
return new Promise(resolve => this.whenFlushedCallbacks.push(resolve));
411
}
412
413
isInMemory(): boolean {
414
return this.options.hint === StorageHint.STORAGE_IN_MEMORY;
415
}
416
}
417
418
export class InMemoryStorageDatabase implements IStorageDatabase {
419
420
readonly onDidChangeItemsExternal = Event.None;
421
422
private readonly items = new Map<string, string>();
423
424
async getItems(): Promise<Map<string, string>> {
425
return this.items;
426
}
427
428
async updateItems(request: IUpdateRequest): Promise<void> {
429
request.insert?.forEach((value, key) => this.items.set(key, value));
430
431
request.delete?.forEach(key => this.items.delete(key));
432
}
433
434
async optimize(): Promise<void> { }
435
async close(): Promise<void> { }
436
}
437
438