forked from jupyterlab/jupyterlab
-
Notifications
You must be signed in to change notification settings - Fork 1
/
statedb.ts
331 lines (292 loc) · 8.71 KB
/
statedb.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { ReadonlyJSONObject, ReadonlyJSONValue } from '@phosphor/coreutils';
import { ISignal, Signal } from '@phosphor/signaling';
import { IDataConnector } from './interfaces';
import { IStateDB } from './tokens';
/**
* The default concrete implementation of a state database.
*/
export class StateDB<T extends ReadonlyJSONValue = ReadonlyJSONValue>
implements IStateDB<T> {
/**
* Create a new state database.
*
* @param options - The instantiation options for a state database.
*/
constructor(options: StateDB.IOptions = {}) {
const { connector, transform } = options;
this._connector = connector || new StateDB.Connector();
this._ready = (transform || Promise.resolve(null)).then(transformation => {
if (!transformation) {
return;
}
const { contents, type } = transformation;
switch (type) {
case 'cancel':
return;
case 'clear':
return this._clear();
case 'merge':
return this._merge(contents || {});
case 'overwrite':
return this._overwrite(contents || {});
default:
return;
}
});
}
/**
* A signal that emits the change type any time a value changes.
*/
get changed(): ISignal<this, StateDB.Change> {
return this._changed;
}
/**
* Clear the entire database.
*/
async clear(): Promise<void> {
await this._ready;
await this._clear();
}
/**
* Retrieve a saved bundle from the database.
*
* @param id - The identifier used to retrieve a data bundle.
*
* @returns A promise that bears a data payload if available.
*
* #### Notes
* The `id` values of stored items in the state database are formatted:
* `'namespace:identifier'`, which is the same convention that command
* identifiers in JupyterLab use as well. While this is not a technical
* requirement for `fetch()`, `remove()`, and `save()`, it *is* necessary for
* using the `list(namespace: string)` method.
*
* The promise returned by this method may be rejected if an error occurs in
* retrieving the data. Non-existence of an `id` will succeed with the `value`
* `undefined`.
*/
async fetch(id: string): Promise<T> {
await this._ready;
return this._fetch(id);
}
/**
* Retrieve all the saved bundles for a namespace.
*
* @param filter - The namespace prefix to retrieve.
*
* @returns A promise that bears a collection of payloads for a namespace.
*
* #### Notes
* Namespaces are entirely conventional entities. The `id` values of stored
* items in the state database are formatted: `'namespace:identifier'`, which
* is the same convention that command identifiers in JupyterLab use as well.
*
* If there are any errors in retrieving the data, they will be logged to the
* console in order to optimistically return any extant data without failing.
* This promise will always succeed.
*/
async list(namespace: string): Promise<{ ids: string[]; values: T[] }> {
await this._ready;
return this._list(namespace);
}
/**
* Remove a value from the database.
*
* @param id - The identifier for the data being removed.
*
* @returns A promise that is rejected if remove fails and succeeds otherwise.
*/
async remove(id: string): Promise<void> {
await this._ready;
await this._remove(id);
this._changed.emit({ id, type: 'remove' });
}
/**
* Save a value in the database.
*
* @param id - The identifier for the data being saved.
*
* @param value - The data being saved.
*
* @returns A promise that is rejected if saving fails and succeeds otherwise.
*
* #### Notes
* The `id` values of stored items in the state database are formatted:
* `'namespace:identifier'`, which is the same convention that command
* identifiers in JupyterLab use as well. While this is not a technical
* requirement for `fetch()`, `remove()`, and `save()`, it *is* necessary for
* using the `list(namespace: string)` method.
*/
async save(id: string, value: T): Promise<void> {
await this._ready;
await this._save(id, value);
this._changed.emit({ id, type: 'save' });
}
/**
* Return a serialized copy of the state database's entire contents.
*
* @returns A promise that resolves with the database contents as JSON.
*/
async toJSON(): Promise<{ readonly [id: string]: T }> {
await this._ready;
const { ids, values } = await this._list();
return values.reduce((acc, val, idx) => {
acc[ids[idx]] = val;
return acc;
}, {} as { [id: string]: T });
}
/**
* Clear the entire database.
*/
private async _clear(): Promise<void> {
await Promise.all((await this._list()).ids.map(id => this._remove(id)));
}
/**
* Fetch a value from the database.
*/
private async _fetch(id: string): Promise<T | undefined> {
const value = await this._connector.fetch(id);
if (value) {
return (JSON.parse(value) as Private.Envelope).v as T;
}
}
/**
* Fetch a list from the database.
*/
private async _list(query?: string): Promise<{ ids: string[]; values: T[] }> {
const { ids, values } = await this._connector.list(query);
return {
ids,
values: values.map(val => (JSON.parse(val) as Private.Envelope).v as T)
};
}
/**
* Merge data into the state database.
*/
private async _merge(contents: { [id: string]: T }): Promise<void> {
await Promise.all(
Object.keys(contents).map(key => this._save(key, contents[key]))
);
}
/**
* Overwrite the entire database with new contents.
*/
private async _overwrite(contents: { [id: string]: T }): Promise<void> {
await this._clear();
await this._merge(contents);
}
/**
* Remove a key in the database.
*/
private async _remove(id: string): Promise<void> {
return this._connector.remove(id);
}
/**
* Save a key and its value in the database.
*/
private async _save(id: string, value: T): Promise<void> {
return this._connector.save(id, JSON.stringify({ v: value }));
}
private _changed = new Signal<this, StateDB.Change>(this);
private _connector: IDataConnector<string>;
private _ready: Promise<void>;
}
/**
* A namespace for StateDB statics.
*/
export namespace StateDB {
/**
* A state database change.
*/
export type Change = {
/**
* The key of the database item that was changed.
*
* #### Notes
* This field is set to `null` for global changes (i.e. `clear`).
*/
id: string | null;
/**
* The type of change.
*/
type: 'clear' | 'remove' | 'save';
};
/**
* A data transformation that can be applied to a state database.
*/
export type DataTransform = {
/*
* The change operation being applied.
*/
type: 'cancel' | 'clear' | 'merge' | 'overwrite';
/**
* The contents of the change operation.
*/
contents: ReadonlyJSONObject | null;
};
/**
* The instantiation options for a state database.
*/
export interface IOptions {
/**
* Optional string key/value connector. Defaults to in-memory connector.
*/
connector?: IDataConnector<string>;
/**
* An optional promise that resolves with a data transformation that is
* applied to the database contents before the database begins resolving
* client requests.
*/
transform?: Promise<DataTransform>;
}
/**
* An in-memory string key/value data connector.
*/
export class Connector implements IDataConnector<string> {
/**
* Retrieve an item from the data connector.
*/
async fetch(id: string): Promise<string> {
return this._storage[id];
}
/**
* Retrieve the list of items available from the data connector.
*/
async list(query = ''): Promise<{ ids: string[]; values: string[] }> {
return Object.keys(this._storage).reduce(
(acc, val) => {
if (val && val.indexOf(query) === 0) {
acc.ids.push(val);
acc.values.push(this._storage[val]);
}
return acc;
},
{ ids: [], values: [] }
);
}
/**
* Remove a value using the data connector.
*/
async remove(id: string): Promise<void> {
delete this._storage[id];
}
/**
* Save a value using the data connector.
*/
async save(id: string, value: string): Promise<void> {
this._storage[id] = value;
}
private _storage: { [key: string]: string } = {};
}
}
/*
* A namespace for private module data.
*/
namespace Private {
/**
* An envelope around a JSON value stored in the state database.
*/
export type Envelope = { readonly v: ReadonlyJSONValue };
}