123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
- import { ISignal, Signal } from '@lumino/signaling';
- import { IDataConnector } from './interfaces';
- import { IStateDB } from './tokens';
- /**
- * The default concrete implementation of a state database.
- */
- export class StateDB<
- T extends ReadonlyPartialJSONValue = ReadonlyPartialJSONValue
- > implements IStateDB<T> {
- /**
- * Create a new state database.
- *
- * @param options - The instantiation options for a state database.
- */
- constructor(options: StateDB.IOptions<T> = {}) {
- const { connector, transform } = options;
- this._connector = connector || new StateDB.Connector();
- if (!transform) {
- this._ready = Promise.resolve(undefined);
- } else {
- this._ready = transform.then(transformation => {
- 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 | undefined> {
- 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(namespace = ''): Promise<{ ids: string[]; values: T[] }> {
- const { ids, values } = await this._connector.list(namespace);
- 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: StateDB.Content<T>): Promise<void> {
- await Promise.all(
- Object.keys(contents).map(
- key => contents[key] && this._save(key, contents[key]!)
- )
- );
- }
- /**
- * Overwrite the entire database with new contents.
- */
- private async _overwrite(contents: StateDB.Content<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<
- T extends ReadonlyPartialJSONValue = ReadonlyPartialJSONValue
- > = {
- /*
- * The change operation being applied.
- */
- type: 'cancel' | 'clear' | 'merge' | 'overwrite';
- /**
- * The contents of the change operation.
- */
- contents: Content<T> | null;
- };
- /**
- * Database content map
- */
- export type Content<T> = { [id: string]: T | undefined };
- /**
- * The instantiation options for a state database.
- */
- export interface IOptions<
- T extends ReadonlyPartialJSONValue = ReadonlyPartialJSONValue
- > {
- /**
- * 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<T>>;
- }
- /**
- * 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.
- *
- * @param namespace - If not empty, only keys whose first token before `:`
- * exactly match `namespace` will be returned, e.g. `foo` in `foo:bar`.
- */
- async list(namespace = ''): Promise<{ ids: string[]; values: string[] }> {
- return Object.keys(this._storage).reduce(
- (acc, val) => {
- if (namespace === '' ? true : namespace === val.split(':')[0]) {
- acc.ids.push(val);
- acc.values.push(this._storage[val]);
- }
- return acc;
- },
- { ids: [] as string[], values: [] as string[] }
- );
- }
- /**
- * 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: ReadonlyPartialJSONValue };
- }
|