statedb.ts 11 KB

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