statedb.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ReadonlyJSONObject,
  5. ReadonlyJSONValue,
  6. Token
  7. } from '@phosphor/coreutils';
  8. import { ISignal, Signal } from '@phosphor/signaling';
  9. import { IDataConnector } from './interfaces';
  10. /* tslint:disable */
  11. /**
  12. * The default state database token.
  13. */
  14. export const IStateDB = new Token<IStateDB>('@jupyterlab/coreutils:IStateDB');
  15. /* tslint:enable */
  16. /**
  17. * The description of a state database.
  18. */
  19. export interface IStateDB<T extends ReadonlyJSONValue = ReadonlyJSONValue>
  20. extends IDataConnector<T> {
  21. /**
  22. * The maximum allowed length of the data after it has been serialized.
  23. */
  24. readonly maxLength: number;
  25. /**
  26. * The namespace prefix for all state database entries.
  27. *
  28. * #### Notes
  29. * This value should be set at instantiation and will only be used
  30. * internally by a state database. That means, for example, that an
  31. * app could have multiple, mutually exclusive state databases.
  32. */
  33. readonly namespace: string;
  34. /**
  35. * Return a serialized copy of the state database's entire contents.
  36. *
  37. * @returns A promise that bears the database contents as JSON.
  38. */
  39. toJSON(): Promise<{ [id: string]: T }>;
  40. }
  41. /**
  42. * The default concrete implementation of a state database.
  43. */
  44. export class StateDB<T extends ReadonlyJSONValue = ReadonlyJSONValue>
  45. implements IStateDB<T> {
  46. /**
  47. * Create a new state database.
  48. *
  49. * @param options - The instantiation options for a state database.
  50. */
  51. constructor(options: StateDB.IOptions) {
  52. const { namespace, transform, windowName } = options;
  53. this.namespace = namespace;
  54. this._window = windowName || '';
  55. this._ready = (transform || Promise.resolve(null)).then(transformation => {
  56. if (!transformation) {
  57. return;
  58. }
  59. const { contents, type } = transformation;
  60. switch (type) {
  61. case 'cancel':
  62. return;
  63. case 'clear':
  64. this._clear();
  65. return;
  66. case 'merge':
  67. this._merge(contents || {});
  68. return;
  69. case 'overwrite':
  70. this._overwrite(contents || {});
  71. return;
  72. default:
  73. return;
  74. }
  75. });
  76. }
  77. /**
  78. * A signal that emits the change type any time a value changes.
  79. */
  80. get changed(): ISignal<this, StateDB.Change> {
  81. return this._changed;
  82. }
  83. /**
  84. * The maximum allowed length of the data after it has been serialized.
  85. */
  86. readonly maxLength: number = 2000;
  87. /**
  88. * The namespace prefix for all state database entries.
  89. *
  90. * #### Notes
  91. * This value should be set at instantiation and will only be used internally
  92. * by a state database. That means, for example, that an app could have
  93. * multiple, mutually exclusive state databases.
  94. */
  95. readonly namespace: string;
  96. /**
  97. * Clear the entire database.
  98. */
  99. clear(silent = false): Promise<void> {
  100. return this._ready.then(() => {
  101. this._clear();
  102. if (silent) {
  103. return;
  104. }
  105. this._changed.emit({ id: null, type: 'clear' });
  106. });
  107. }
  108. /**
  109. * Retrieve a saved bundle from the database.
  110. *
  111. * @param id - The identifier used to retrieve a data bundle.
  112. *
  113. * @returns A promise that bears a data payload if available.
  114. *
  115. * #### Notes
  116. * The `id` values of stored items in the state database are formatted:
  117. * `'namespace:identifier'`, which is the same convention that command
  118. * identifiers in JupyterLab use as well. While this is not a technical
  119. * requirement for `fetch()`, `remove()`, and `save()`, it *is* necessary for
  120. * using the `list(namespace: string)` method.
  121. *
  122. * The promise returned by this method may be rejected if an error occurs in
  123. * retrieving the data. Non-existence of an `id` will succeed with the `value`
  124. * `undefined`.
  125. */
  126. async fetch(id: string): Promise<T> {
  127. const value = await this._ready.then(() => this._fetch(id));
  128. return value as T;
  129. }
  130. /**
  131. * Retrieve all the saved bundles for a namespace.
  132. *
  133. * @param filter - The namespace prefix to retrieve.
  134. *
  135. * @returns A promise that bears a collection of payloads for a namespace.
  136. *
  137. * #### Notes
  138. * Namespaces are entirely conventional entities. The `id` values of stored
  139. * items in the state database are formatted: `'namespace:identifier'`, which
  140. * is the same convention that command identifiers in JupyterLab use as well.
  141. *
  142. * If there are any errors in retrieving the data, they will be logged to the
  143. * console in order to optimistically return any extant data without failing.
  144. * This promise will always succeed.
  145. */
  146. list(namespace: string): Promise<{ ids: string[]; values: T[] }> {
  147. return this._ready.then(() => {
  148. const prefix = `${this._window}:${this.namespace}:`;
  149. const mask = (key: string) => key.replace(prefix, '');
  150. return Private.fetchNamespace<T>(`${prefix}${namespace}:`, mask);
  151. });
  152. }
  153. /**
  154. * Remove a value from the database.
  155. *
  156. * @param id - The identifier for the data being removed.
  157. *
  158. * @returns A promise that is rejected if remove fails and succeeds otherwise.
  159. */
  160. remove(id: string): Promise<void> {
  161. return this._ready.then(() => {
  162. this._remove(id);
  163. this._changed.emit({ id, type: 'remove' });
  164. });
  165. }
  166. /**
  167. * Save a value in the database.
  168. *
  169. * @param id - The identifier for the data being saved.
  170. *
  171. * @param value - The data being saved.
  172. *
  173. * @returns A promise that is rejected if saving fails and succeeds otherwise.
  174. *
  175. * #### Notes
  176. * The `id` values of stored items in the state database are formatted:
  177. * `'namespace:identifier'`, which is the same convention that command
  178. * identifiers in JupyterLab use as well. While this is not a technical
  179. * requirement for `fetch()`, `remove()`, and `save()`, it *is* necessary for
  180. * using the `list(namespace: string)` method.
  181. */
  182. save(id: string, value: T): Promise<void> {
  183. return this._ready.then(() => {
  184. this._save(id, value);
  185. this._changed.emit({ id, type: 'save' });
  186. });
  187. }
  188. /**
  189. * Return a serialized copy of the state database's entire contents.
  190. *
  191. * @returns A promise that bears the database contents as JSON.
  192. */
  193. toJSON(): Promise<{ [id: string]: T }> {
  194. return this._ready.then(() => {
  195. const prefix = `${this._window}:${this.namespace}:`;
  196. const mask = (key: string) => key.replace(prefix, '');
  197. return Private.toJSON<T>(prefix, mask);
  198. });
  199. }
  200. /**
  201. * Clear the entire database.
  202. *
  203. * #### Notes
  204. * Unlike the public `clear` method, this method is synchronous.
  205. */
  206. private _clear(): void {
  207. const { localStorage } = window;
  208. const prefix = `${this._window}:${this.namespace}:`;
  209. let i = localStorage.length;
  210. while (i) {
  211. let key = localStorage.key(--i);
  212. if (key && key.indexOf(prefix) === 0) {
  213. localStorage.removeItem(key);
  214. }
  215. }
  216. }
  217. /**
  218. * Fetch a value from the database.
  219. *
  220. * #### Notes
  221. * Unlike the public `fetch` method, this method is synchronous.
  222. */
  223. private _fetch(id: string): ReadonlyJSONValue | undefined {
  224. const key = `${this._window}:${this.namespace}:${id}`;
  225. const value = window.localStorage.getItem(key);
  226. if (value) {
  227. const envelope = JSON.parse(value) as Private.Envelope;
  228. return envelope.v;
  229. }
  230. return undefined;
  231. }
  232. /**
  233. * Merge data into the state database.
  234. */
  235. private _merge(contents: ReadonlyJSONObject): void {
  236. Object.keys(contents).forEach(key => {
  237. this._save(key, contents[key]);
  238. });
  239. }
  240. /**
  241. * Overwrite the entire database with new contents.
  242. */
  243. private _overwrite(contents: ReadonlyJSONObject): void {
  244. this._clear();
  245. this._merge(contents);
  246. }
  247. /**
  248. * Remove a key in the database.
  249. *
  250. * #### Notes
  251. * Unlike the public `remove` method, this method is synchronous.
  252. */
  253. private _remove(id: string): void {
  254. const key = `${this._window}:${this.namespace}:${id}`;
  255. window.localStorage.removeItem(key);
  256. }
  257. /**
  258. * Save a key and its value in the database.
  259. *
  260. * #### Notes
  261. * Unlike the public `save` method, this method is synchronous.
  262. */
  263. private _save(id: string, value: ReadonlyJSONValue): void {
  264. const key = `${this._window}:${this.namespace}:${id}`;
  265. const envelope: Private.Envelope = { v: value };
  266. const serialized = JSON.stringify(envelope);
  267. const length = serialized.length;
  268. const max = this.maxLength;
  269. if (length > max) {
  270. throw new Error(`Data length (${length}) exceeds maximum (${max})`);
  271. }
  272. window.localStorage.setItem(key, serialized);
  273. }
  274. private _changed = new Signal<this, StateDB.Change>(this);
  275. private _ready: Promise<void>;
  276. private _window: string;
  277. }
  278. /**
  279. * A namespace for StateDB statics.
  280. */
  281. export namespace StateDB {
  282. /**
  283. * A state database change.
  284. */
  285. export type Change = {
  286. /**
  287. * The key of the database item that was changed.
  288. *
  289. * #### Notes
  290. * This field is set to `null` for global changes (i.e. `clear`).
  291. */
  292. id: string | null;
  293. /**
  294. * The type of change.
  295. */
  296. type: 'clear' | 'remove' | 'save';
  297. };
  298. /**
  299. * A data transformation that can be applied to a state database.
  300. */
  301. export type DataTransform = {
  302. /*
  303. * The change operation being applied.
  304. */
  305. type: 'cancel' | 'clear' | 'merge' | 'overwrite';
  306. /**
  307. * The contents of the change operation.
  308. */
  309. contents: ReadonlyJSONObject | null;
  310. };
  311. /**
  312. * The instantiation options for a state database.
  313. */
  314. export interface IOptions {
  315. /**
  316. * The namespace prefix for all state database entries.
  317. */
  318. namespace: string;
  319. /**
  320. * An optional promise that resolves with a data transformation that is
  321. * applied to the database contents before the database begins resolving
  322. * client requests.
  323. */
  324. transform?: Promise<DataTransform>;
  325. /**
  326. * An optional name for the application window.
  327. *
  328. * #### Notes
  329. * In environments where multiple windows can instantiate a state database,
  330. * a window name is necessary to prefix all keys that are stored within the
  331. * local storage that is shared by all windows. In JupyterLab, this window
  332. * name is generated by the `IWindowResolver` extension.
  333. */
  334. windowName?: string;
  335. }
  336. }
  337. /*
  338. * A namespace for private module data.
  339. */
  340. namespace Private {
  341. /**
  342. * An envelope around a JSON value stored in the state database.
  343. */
  344. export type Envelope = { readonly v: ReadonlyJSONValue };
  345. /**
  346. * Retrieve all the saved bundles for a given namespace in local storage.
  347. *
  348. * @param prefix - The namespace to retrieve.
  349. *
  350. * @param mask - Optional mask function to transform each key retrieved.
  351. *
  352. * @returns A collection of data payloads for a given prefix.
  353. *
  354. * #### Notes
  355. * If there are any errors in retrieving the data, they will be logged to the
  356. * console in order to optimistically return any extant data without failing.
  357. */
  358. export function fetchNamespace<
  359. T extends ReadonlyJSONValue = ReadonlyJSONValue
  360. >(
  361. namespace: string,
  362. mask: (key: string) => string = key => key
  363. ): { ids: string[]; values: T[] } {
  364. const { localStorage } = window;
  365. let ids: string[] = [];
  366. let values: T[] = [];
  367. let i = localStorage.length;
  368. while (i) {
  369. let key = localStorage.key(--i);
  370. if (key && key.indexOf(namespace) === 0) {
  371. let value = localStorage.getItem(key);
  372. try {
  373. let envelope = JSON.parse(value) as Envelope;
  374. let id = mask(key);
  375. values[ids.push(id) - 1] = envelope ? (envelope.v as T) : undefined;
  376. } catch (error) {
  377. console.warn(error);
  378. localStorage.removeItem(key);
  379. }
  380. }
  381. }
  382. return { ids, values };
  383. }
  384. /**
  385. * Return a serialized copy of a namespace's contents from local storage.
  386. *
  387. * @returns The namespace contents as JSON.
  388. */
  389. export function toJSON<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
  390. namespace: string,
  391. mask: (key: string) => string = key => key
  392. ): { [id: string]: T } {
  393. const { ids, values } = fetchNamespace<T>(namespace, mask);
  394. return values.reduce(
  395. (acc, val, idx) => {
  396. acc[ids[idx]] = val;
  397. return acc;
  398. },
  399. {} as { [id: string]: T }
  400. );
  401. }
  402. }