123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- ReadonlyJSONObject,
- ReadonlyJSONValue,
- Token
- } from '@phosphor/coreutils';
- import { ISignal, Signal } from '@phosphor/signaling';
- import { IDataConnector } from './interfaces';
- /* tslint:disable */
- /**
- * The default state database token.
- */
- export const IStateDB = new Token<IStateDB>('@jupyterlab/coreutils:IStateDB');
- /* tslint:enable */
- /**
- * The description of a state database.
- */
- export interface IStateDB<T extends ReadonlyJSONValue = ReadonlyJSONValue>
- extends IDataConnector<T> {
- /**
- * The maximum allowed length of the data after it has been serialized.
- */
- readonly maxLength: number;
- /**
- * The namespace prefix for all state database entries.
- *
- * #### Notes
- * This value should be set at instantiation and will only be used
- * internally by a state database. That means, for example, that an
- * app could have multiple, mutually exclusive state databases.
- */
- readonly namespace: string;
- /**
- * Return a serialized copy of the state database's entire contents.
- *
- * @returns A promise that bears the database contents as JSON.
- */
- toJSON(): Promise<{ [id: string]: T }>;
- }
- /**
- * 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 { namespace, transform, windowName } = options;
- this.namespace = namespace;
- this._window = windowName || '';
- this._ready = (transform || Promise.resolve(null)).then(transformation => {
- if (!transformation) {
- return;
- }
- const { contents, type } = transformation;
- switch (type) {
- case 'cancel':
- return;
- case 'clear':
- this._clear();
- return;
- case 'merge':
- this._merge(contents || {});
- return;
- case 'overwrite':
- this._overwrite(contents || {});
- return;
- default:
- return;
- }
- });
- }
- /**
- * A signal that emits the change type any time a value changes.
- */
- get changed(): ISignal<this, StateDB.Change> {
- return this._changed;
- }
- /**
- * The maximum allowed length of the data after it has been serialized.
- */
- readonly maxLength: number = 2000;
- /**
- * The namespace prefix for all state database entries.
- *
- * #### Notes
- * This value should be set at instantiation and will only be used internally
- * by a state database. That means, for example, that an app could have
- * multiple, mutually exclusive state databases.
- */
- readonly namespace: string;
- /**
- * Clear the entire database.
- */
- clear(silent = false): Promise<void> {
- return this._ready.then(() => {
- this._clear();
- if (silent) {
- return;
- }
- this._changed.emit({ id: null, type: '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> {
- const value = await this._ready.then(() => this._fetch(id));
- return value as T;
- }
- /**
- * 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.
- */
- list(namespace: string): Promise<{ ids: string[]; values: T[] }> {
- return this._ready.then(() => {
- const prefix = `${this._window}:${this.namespace}:`;
- const mask = (key: string) => key.replace(prefix, '');
- return Private.fetchNamespace<T>(`${prefix}${namespace}:`, mask);
- });
- }
- /**
- * 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.
- */
- remove(id: string): Promise<void> {
- return this._ready.then(() => {
- 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.
- */
- save(id: string, value: T): Promise<void> {
- return this._ready.then(() => {
- 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 bears the database contents as JSON.
- */
- toJSON(): Promise<{ [id: string]: T }> {
- return this._ready.then(() => {
- const prefix = `${this._window}:${this.namespace}:`;
- const mask = (key: string) => key.replace(prefix, '');
- return Private.toJSON<T>(prefix, mask);
- });
- }
- /**
- * Clear the entire database.
- *
- * #### Notes
- * Unlike the public `clear` method, this method is synchronous.
- */
- private _clear(): void {
- const { localStorage } = window;
- const prefix = `${this._window}:${this.namespace}:`;
- let i = localStorage.length;
- while (i) {
- let key = localStorage.key(--i);
- if (key && key.indexOf(prefix) === 0) {
- localStorage.removeItem(key);
- }
- }
- }
- /**
- * Fetch a value from the database.
- *
- * #### Notes
- * Unlike the public `fetch` method, this method is synchronous.
- */
- private _fetch(id: string): ReadonlyJSONValue | undefined {
- const key = `${this._window}:${this.namespace}:${id}`;
- const value = window.localStorage.getItem(key);
- if (value) {
- const envelope = JSON.parse(value) as Private.Envelope;
- return envelope.v;
- }
- return undefined;
- }
- /**
- * Merge data into the state database.
- */
- private _merge(contents: ReadonlyJSONObject): void {
- Object.keys(contents).forEach(key => {
- this._save(key, contents[key]);
- });
- }
- /**
- * Overwrite the entire database with new contents.
- */
- private _overwrite(contents: ReadonlyJSONObject): void {
- this._clear();
- this._merge(contents);
- }
- /**
- * Remove a key in the database.
- *
- * #### Notes
- * Unlike the public `remove` method, this method is synchronous.
- */
- private _remove(id: string): void {
- const key = `${this._window}:${this.namespace}:${id}`;
- window.localStorage.removeItem(key);
- }
- /**
- * Save a key and its value in the database.
- *
- * #### Notes
- * Unlike the public `save` method, this method is synchronous.
- */
- private _save(id: string, value: ReadonlyJSONValue): void {
- const key = `${this._window}:${this.namespace}:${id}`;
- const envelope: Private.Envelope = { v: value };
- const serialized = JSON.stringify(envelope);
- const length = serialized.length;
- const max = this.maxLength;
- if (length > max) {
- throw new Error(`Data length (${length}) exceeds maximum (${max})`);
- }
- window.localStorage.setItem(key, serialized);
- }
- private _changed = new Signal<this, StateDB.Change>(this);
- private _ready: Promise<void>;
- private _window: string;
- }
- /**
- * 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 {
- /**
- * The namespace prefix for all state database entries.
- */
- namespace: 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 optional name for the application window.
- *
- * #### Notes
- * In environments where multiple windows can instantiate a state database,
- * a window name is necessary to prefix all keys that are stored within the
- * local storage that is shared by all windows. In JupyterLab, this window
- * name is generated by the `IWindowResolver` extension.
- */
- windowName?: 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 };
- /**
- * Retrieve all the saved bundles for a given namespace in local storage.
- *
- * @param prefix - The namespace to retrieve.
- *
- * @param mask - Optional mask function to transform each key retrieved.
- *
- * @returns A collection of data payloads for a given prefix.
- *
- * #### Notes
- * 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.
- */
- export function fetchNamespace<
- T extends ReadonlyJSONValue = ReadonlyJSONValue
- >(
- namespace: string,
- mask: (key: string) => string = key => key
- ): { ids: string[]; values: T[] } {
- const { localStorage } = window;
- let ids: string[] = [];
- let values: T[] = [];
- let i = localStorage.length;
- while (i) {
- let key = localStorage.key(--i);
- if (key && key.indexOf(namespace) === 0) {
- let value = localStorage.getItem(key);
- try {
- let envelope = JSON.parse(value) as Envelope;
- let id = mask(key);
- values[ids.push(id) - 1] = envelope ? (envelope.v as T) : undefined;
- } catch (error) {
- console.warn(error);
- localStorage.removeItem(key);
- }
- }
- }
- return { ids, values };
- }
- /**
- * Return a serialized copy of a namespace's contents from local storage.
- *
- * @returns The namespace contents as JSON.
- */
- export function toJSON<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
- namespace: string,
- mask: (key: string) => string = key => key
- ): { [id: string]: T } {
- const { ids, values } = fetchNamespace<T>(namespace, mask);
- return values.reduce(
- (acc, val, idx) => {
- acc[ids[idx]] = val;
- return acc;
- },
- {} as { [id: string]: T }
- );
- }
- }
|