Skip to content

Commit a65e9ca

Browse files
author
Benjamin Pasero
committed
web - first cut user data provider storage service
1 parent c212dda commit a65e9ca

14 files changed

Lines changed: 650 additions & 543 deletions

File tree

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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 { Disposable, IDisposable } from 'vs/base/common/lifecycle';
7+
import { Emitter, Event } from 'vs/base/common/event';
8+
import { ThrottledDelayer } from 'vs/base/common/async';
9+
import { isUndefinedOrNull } from 'vs/base/common/types';
10+
11+
export enum StorageHint {
12+
13+
// A hint to the storage that the storage
14+
// does not exist on disk yet. This allows
15+
// the storage library to improve startup
16+
// time by not checking the storage for data.
17+
STORAGE_DOES_NOT_EXIST
18+
}
19+
20+
export interface IStorageOptions {
21+
hint?: StorageHint;
22+
}
23+
24+
export interface IUpdateRequest {
25+
insert?: Map<string, string>;
26+
delete?: Set<string>;
27+
}
28+
29+
export interface IStorageItemsChangeEvent {
30+
items: Map<string, string>;
31+
}
32+
33+
export interface IStorageDatabase {
34+
35+
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
36+
37+
getItems(): Promise<Map<string, string>>;
38+
updateItems(request: IUpdateRequest): Promise<void>;
39+
40+
close(recovery?: () => Map<string, string>): Promise<void>;
41+
}
42+
43+
export interface IStorage extends IDisposable {
44+
45+
readonly items: Map<string, string>;
46+
readonly size: number;
47+
readonly onDidChangeStorage: Event<string>;
48+
49+
init(): Promise<void>;
50+
51+
get(key: string, fallbackValue: string): string;
52+
get(key: string, fallbackValue?: string): string | undefined;
53+
54+
getBoolean(key: string, fallbackValue: boolean): boolean;
55+
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
56+
57+
getNumber(key: string, fallbackValue: number): number;
58+
getNumber(key: string, fallbackValue?: number): number | undefined;
59+
60+
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
61+
delete(key: string): Promise<void>;
62+
63+
close(): Promise<void>;
64+
}
65+
66+
enum StorageState {
67+
None,
68+
Initialized,
69+
Closed
70+
}
71+
72+
export class Storage extends Disposable implements IStorage {
73+
74+
private static readonly DEFAULT_FLUSH_DELAY = 100;
75+
76+
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
77+
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
78+
79+
private state = StorageState.None;
80+
81+
private cache: Map<string, string> = new Map<string, string>();
82+
83+
private flushDelayer: ThrottledDelayer<void>;
84+
85+
private pendingDeletes: Set<string> = new Set<string>();
86+
private pendingInserts: Map<string, string> = new Map();
87+
88+
constructor(
89+
protected database: IStorageDatabase,
90+
private options: IStorageOptions = Object.create(null)
91+
) {
92+
super();
93+
94+
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
95+
96+
this.registerListeners();
97+
}
98+
99+
private registerListeners(): void {
100+
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
101+
}
102+
103+
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
104+
// items that change external require us to update our
105+
// caches with the values. we just accept the value and
106+
// emit an event if there is a change.
107+
e.items.forEach((value, key) => this.accept(key, value));
108+
}
109+
110+
private accept(key: string, value: string): void {
111+
if (this.state === StorageState.Closed) {
112+
return; // Return early if we are already closed
113+
}
114+
115+
let changed = false;
116+
117+
// Item got removed, check for deletion
118+
if (isUndefinedOrNull(value)) {
119+
changed = this.cache.delete(key);
120+
}
121+
122+
// Item got updated, check for change
123+
else {
124+
const currentValue = this.cache.get(key);
125+
if (currentValue !== value) {
126+
this.cache.set(key, value);
127+
changed = true;
128+
}
129+
}
130+
131+
// Signal to outside listeners
132+
if (changed) {
133+
this._onDidChangeStorage.fire(key);
134+
}
135+
}
136+
137+
get items(): Map<string, string> {
138+
return this.cache;
139+
}
140+
141+
get size(): number {
142+
return this.cache.size;
143+
}
144+
145+
async init(): Promise<void> {
146+
if (this.state !== StorageState.None) {
147+
return Promise.resolve(); // either closed or already initialized
148+
}
149+
150+
this.state = StorageState.Initialized;
151+
152+
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
153+
// return early if we know the storage file does not exist. this is a performance
154+
// optimization to not load all items of the underlying storage if we know that
155+
// there can be no items because the storage does not exist.
156+
return Promise.resolve();
157+
}
158+
159+
this.cache = await this.database.getItems();
160+
}
161+
162+
get(key: string, fallbackValue: string): string;
163+
get(key: string, fallbackValue?: string): string | undefined;
164+
get(key: string, fallbackValue?: string): string | undefined {
165+
const value = this.cache.get(key);
166+
167+
if (isUndefinedOrNull(value)) {
168+
return fallbackValue;
169+
}
170+
171+
return value;
172+
}
173+
174+
getBoolean(key: string, fallbackValue: boolean): boolean;
175+
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
176+
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
177+
const value = this.get(key);
178+
179+
if (isUndefinedOrNull(value)) {
180+
return fallbackValue;
181+
}
182+
183+
return value === 'true';
184+
}
185+
186+
getNumber(key: string, fallbackValue: number): number;
187+
getNumber(key: string, fallbackValue?: number): number | undefined;
188+
getNumber(key: string, fallbackValue?: number): number | undefined {
189+
const value = this.get(key);
190+
191+
if (isUndefinedOrNull(value)) {
192+
return fallbackValue;
193+
}
194+
195+
return parseInt(value, 10);
196+
}
197+
198+
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
199+
if (this.state === StorageState.Closed) {
200+
return Promise.resolve(); // Return early if we are already closed
201+
}
202+
203+
// We remove the key for undefined/null values
204+
if (isUndefinedOrNull(value)) {
205+
return this.delete(key);
206+
}
207+
208+
// Otherwise, convert to String and store
209+
const valueStr = String(value);
210+
211+
// Return early if value already set
212+
const currentValue = this.cache.get(key);
213+
if (currentValue === valueStr) {
214+
return Promise.resolve();
215+
}
216+
217+
// Update in cache and pending
218+
this.cache.set(key, valueStr);
219+
this.pendingInserts.set(key, valueStr);
220+
this.pendingDeletes.delete(key);
221+
222+
// Event
223+
this._onDidChangeStorage.fire(key);
224+
225+
// Accumulate work by scheduling after timeout
226+
return this.flushDelayer.trigger(() => this.flushPending());
227+
}
228+
229+
delete(key: string): Promise<void> {
230+
if (this.state === StorageState.Closed) {
231+
return Promise.resolve(); // Return early if we are already closed
232+
}
233+
234+
// Remove from cache and add to pending
235+
const wasDeleted = this.cache.delete(key);
236+
if (!wasDeleted) {
237+
return Promise.resolve(); // Return early if value already deleted
238+
}
239+
240+
if (!this.pendingDeletes.has(key)) {
241+
this.pendingDeletes.add(key);
242+
}
243+
244+
this.pendingInserts.delete(key);
245+
246+
// Event
247+
this._onDidChangeStorage.fire(key);
248+
249+
// Accumulate work by scheduling after timeout
250+
return this.flushDelayer.trigger(() => this.flushPending());
251+
}
252+
253+
async close(): Promise<void> {
254+
if (this.state === StorageState.Closed) {
255+
return Promise.resolve(); // return if already closed
256+
}
257+
258+
// Update state
259+
this.state = StorageState.Closed;
260+
261+
// Trigger new flush to ensure data is persisted and then close
262+
// even if there is an error flushing. We must always ensure
263+
// the DB is closed to avoid corruption.
264+
//
265+
// Recovery: we pass our cache over as recovery option in case
266+
// the DB is not healthy.
267+
try {
268+
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
269+
} catch (error) {
270+
// Ignore
271+
}
272+
273+
await this.database.close(() => this.cache);
274+
}
275+
276+
private flushPending(): Promise<void> {
277+
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
278+
return Promise.resolve(); // return early if nothing to do
279+
}
280+
281+
// Get pending data
282+
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
283+
284+
// Reset pending data for next run
285+
this.pendingDeletes = new Set<string>();
286+
this.pendingInserts = new Map<string, string>();
287+
288+
// Update in storage
289+
return this.database.updateItems(updateRequest);
290+
}
291+
}
292+
293+
export class InMemoryStorageDatabase implements IStorageDatabase {
294+
295+
readonly onDidChangeItemsExternal = Event.None;
296+
297+
private items = new Map<string, string>();
298+
299+
getItems(): Promise<Map<string, string>> {
300+
return Promise.resolve(this.items);
301+
}
302+
303+
updateItems(request: IUpdateRequest): Promise<void> {
304+
if (request.insert) {
305+
request.insert.forEach((value, key) => this.items.set(key, value));
306+
}
307+
308+
if (request.delete) {
309+
request.delete.forEach(key => this.items.delete(key));
310+
}
311+
312+
return Promise.resolve();
313+
}
314+
315+
close(): Promise<void> {
316+
return Promise.resolve();
317+
}
318+
}

0 commit comments

Comments
 (0)