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