statedb.ts 9.2 KB

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