common.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import { simulate } from 'simulate-event';
  2. import { ServiceManager, Session } from '@jupyterlab/services';
  3. import { SessionContext } from '@jupyterlab/apputils';
  4. import { PromiseDelegate, UUID } from '@lumino/coreutils';
  5. import { ISignal, Signal } from '@lumino/signaling';
  6. import {
  7. Context,
  8. DocumentRegistry,
  9. TextModelFactory
  10. } from '@jupyterlab/docregistry';
  11. import { INotebookModel, NotebookModelFactory } from '@jupyterlab/notebook';
  12. /**
  13. * Test a single emission from a signal.
  14. *
  15. * @param signal - The signal we are listening to.
  16. * @param find - An optional function to determine which emission to test,
  17. * defaulting to the first emission.
  18. * @param test - An optional function which contains the tests for the emission, and should throw an error if the tests fail.
  19. * @param value - An optional value that the promise resolves to if the test is
  20. * successful.
  21. *
  22. * @returns a promise that rejects if the function throws an error (e.g., if an
  23. * expect test doesn't pass), and resolves otherwise.
  24. *
  25. * #### Notes
  26. * The first emission for which the find function returns true will be tested in
  27. * the test function. If the find function is not given, the first signal
  28. * emission will be tested.
  29. *
  30. * You can test to see if any signal comes which matches a criteria by just
  31. * giving a find function. You can test the very first signal by just giving a
  32. * test function. And you can test the first signal matching the find criteria
  33. * by giving both.
  34. *
  35. * The reason this function is asynchronous is so that the thing causing the
  36. * signal emission (such as a websocket message) can be asynchronous.
  37. */
  38. export async function testEmission<T, U, V>(
  39. signal: ISignal<T, U>,
  40. options: {
  41. find?: (a: T, b: U) => boolean;
  42. test?: (a: T, b: U) => void;
  43. value?: V;
  44. } = {}
  45. ): Promise<V | undefined> {
  46. const done = new PromiseDelegate<V | undefined>();
  47. const object = {};
  48. signal.connect((sender: T, args: U) => {
  49. if (options.find?.(sender, args) ?? true) {
  50. try {
  51. Signal.disconnectReceiver(object);
  52. if (options.test) {
  53. options.test(sender, args);
  54. }
  55. } catch (e) {
  56. done.reject(e);
  57. }
  58. done.resolve(options.value ?? undefined);
  59. }
  60. }, object);
  61. return done.promise;
  62. }
  63. /**
  64. * Expect a failure on a promise with the given message.
  65. */
  66. export async function expectFailure(
  67. promise: Promise<any>,
  68. message?: string
  69. ): Promise<void> {
  70. let called = false;
  71. try {
  72. await promise;
  73. called = true;
  74. } catch (err) {
  75. if (message && err.message.indexOf(message) === -1) {
  76. throw Error(`Error "${message}" not in: "${err.message}"`);
  77. }
  78. }
  79. if (called) {
  80. throw Error(`Failure was not triggered, message was: ${message}`);
  81. }
  82. }
  83. /**
  84. * Do something in the future ensuring total ordering with respect to promises.
  85. */
  86. export async function doLater(cb: () => void): Promise<void> {
  87. await Promise.resolve(void 0);
  88. cb();
  89. }
  90. /**
  91. * Convert a signal into an array of promises.
  92. *
  93. * @param signal - The signal we are listening to.
  94. * @param numberValues - The number of values to store.
  95. *
  96. * @returns a Promise that resolves with an array of `(sender, args)` pairs.
  97. */
  98. export function signalToPromises<T, U>(
  99. signal: ISignal<T, U>,
  100. numberValues: number
  101. ): Promise<[T, U]>[] {
  102. const values: Promise<[T, U]>[] = new Array(numberValues);
  103. const resolvers: Array<(value: [T, U]) => void> = new Array(numberValues);
  104. for (let i = 0; i < numberValues; i++) {
  105. values[i] = new Promise<[T, U]>(resolve => {
  106. resolvers[i] = resolve;
  107. });
  108. }
  109. let current = 0;
  110. function slot(sender: T, args: U) {
  111. resolvers[current++]([sender, args]);
  112. if (current === numberValues) {
  113. cleanup();
  114. }
  115. }
  116. signal.connect(slot);
  117. function cleanup() {
  118. signal.disconnect(slot);
  119. }
  120. return values;
  121. }
  122. /**
  123. * Convert a signal into a promise for the first emitted value.
  124. *
  125. * @param signal - The signal we are listening to.
  126. *
  127. * @returns a Promise that resolves with a `(sender, args)` pair.
  128. */
  129. export function signalToPromise<T, U>(signal: ISignal<T, U>): Promise<[T, U]> {
  130. return signalToPromises(signal, 1)[0];
  131. }
  132. /**
  133. * Test to see if a promise is fulfilled.
  134. *
  135. * @param delay - optional delay in milliseconds before checking
  136. * @returns true if the promise is fulfilled (either resolved or rejected), and
  137. * false if the promise is still pending.
  138. */
  139. export async function isFulfilled<T>(
  140. p: PromiseLike<T>,
  141. delay = 0
  142. ): Promise<boolean> {
  143. const x = Object.create(null);
  144. let race: any;
  145. if (delay > 0) {
  146. race = sleep(delay, x);
  147. } else {
  148. race = x;
  149. }
  150. const result = await Promise.race([p, race]).catch(() => false);
  151. return result !== x;
  152. }
  153. /**
  154. * Convert a requestAnimationFrame into a Promise.
  155. */
  156. export function framePromise(): Promise<void> {
  157. const done = new PromiseDelegate<void>();
  158. requestAnimationFrame(() => {
  159. done.resolve(void 0);
  160. });
  161. return done.promise;
  162. }
  163. /**
  164. * Return a promise that resolves in the given milliseconds with the given value.
  165. */
  166. export function sleep(milliseconds?: number): Promise<void>;
  167. export function sleep<T>(milliseconds: number, value: T): Promise<T>;
  168. export function sleep<T>(
  169. milliseconds: number = 0,
  170. value?: any
  171. ): Promise<T> | Promise<void> {
  172. return new Promise<T>((resolve, reject) => {
  173. setTimeout(() => {
  174. resolve(value);
  175. }, milliseconds);
  176. });
  177. }
  178. /**
  179. * Create a client session object.
  180. */
  181. export async function createSessionContext(
  182. options: Partial<SessionContext.IOptions> = {}
  183. ): Promise<SessionContext> {
  184. const manager = options.sessionManager ?? Private.getManager().sessions;
  185. const specsManager = options.specsManager ?? Private.getManager().kernelspecs;
  186. await Promise.all([manager.ready, specsManager.ready]);
  187. return new SessionContext({
  188. sessionManager: manager,
  189. specsManager,
  190. path: options.path ?? UUID.uuid4(),
  191. name: options.name,
  192. type: options.type,
  193. kernelPreference: options.kernelPreference ?? {
  194. shouldStart: true,
  195. canStart: true,
  196. name: specsManager.specs?.default
  197. }
  198. });
  199. }
  200. /**
  201. * Create a session and return a session connection.
  202. */
  203. export async function createSession(
  204. options: Session.ISessionOptions
  205. ): Promise<Session.ISessionConnection> {
  206. const manager = Private.getManager().sessions;
  207. await manager.ready;
  208. return manager.startNew(options);
  209. }
  210. /**
  211. * Create a context for a file.
  212. */
  213. export function createFileContext(
  214. path: string = UUID.uuid4() + '.txt',
  215. manager: ServiceManager.IManager = Private.getManager()
  216. ): Context<DocumentRegistry.IModel> {
  217. const factory = Private.textFactory;
  218. return new Context({ manager, factory, path });
  219. }
  220. export async function createFileContextWithKernel(
  221. path: string = UUID.uuid4() + '.txt',
  222. manager: ServiceManager.IManager = Private.getManager()
  223. ) {
  224. const factory = Private.textFactory;
  225. const specsManager = manager.kernelspecs;
  226. await specsManager.ready;
  227. return new Context({
  228. manager,
  229. factory,
  230. path,
  231. kernelPreference: {
  232. shouldStart: true,
  233. canStart: true,
  234. name: specsManager.specs?.default
  235. }
  236. });
  237. }
  238. /**
  239. * Create and initialize context for a notebook.
  240. */
  241. export async function initNotebookContext(
  242. options: {
  243. path?: string;
  244. manager?: ServiceManager.IManager;
  245. startKernel?: boolean;
  246. } = {}
  247. ): Promise<Context<INotebookModel>> {
  248. const factory = Private.notebookFactory;
  249. const manager = options.manager || Private.getManager();
  250. const path = options.path || UUID.uuid4() + '.ipynb';
  251. console.debug(
  252. 'Initializing notebook context for',
  253. path,
  254. 'kernel:',
  255. options.startKernel
  256. );
  257. const startKernel =
  258. options.startKernel === undefined ? false : options.startKernel;
  259. await manager.ready;
  260. const context = new Context({
  261. manager,
  262. factory,
  263. path,
  264. kernelPreference: {
  265. shouldStart: startKernel,
  266. canStart: startKernel,
  267. shutdownOnDispose: true,
  268. name: manager.kernelspecs.specs?.default
  269. }
  270. });
  271. await context.initialize(true);
  272. if (startKernel) {
  273. await context.sessionContext.initialize();
  274. await context.sessionContext.session?.kernel?.info;
  275. }
  276. return context;
  277. }
  278. /**
  279. * Wait for a dialog to be attached to an element.
  280. */
  281. export async function waitForDialog(
  282. host: HTMLElement = document.body,
  283. timeout: number = 250
  284. ): Promise<void> {
  285. const interval = 25;
  286. const limit = Math.floor(timeout / interval);
  287. for (let counter = 0; counter < limit; counter++) {
  288. if (host.getElementsByClassName('jp-Dialog')[0]) {
  289. return;
  290. }
  291. await sleep(interval);
  292. }
  293. throw new Error('Dialog not found');
  294. }
  295. /**
  296. * Accept a dialog after it is attached by accepting the default button.
  297. */
  298. export async function acceptDialog(
  299. host: HTMLElement = document.body,
  300. timeout: number = 250
  301. ): Promise<void> {
  302. await waitForDialog(host, timeout);
  303. const node = host.getElementsByClassName('jp-Dialog')[0];
  304. if (node) {
  305. simulate(node as HTMLElement, 'keydown', { keyCode: 13 });
  306. }
  307. }
  308. /**
  309. * Click on the warning button in a dialog after it is attached
  310. */
  311. export async function dangerDialog(
  312. host: HTMLElement = document.body,
  313. timeout: number = 250
  314. ): Promise<void> {
  315. await waitForDialog(host, timeout);
  316. const node = host.getElementsByClassName('jp-mod-warn')[0];
  317. if (node) {
  318. simulate(node as HTMLElement, 'click', { button: 1 });
  319. }
  320. }
  321. /**
  322. * Dismiss a dialog after it is attached.
  323. *
  324. * #### Notes
  325. * This promise will always resolve successfully.
  326. */
  327. export async function dismissDialog(
  328. host: HTMLElement = document.body,
  329. timeout: number = 250
  330. ): Promise<void> {
  331. try {
  332. await waitForDialog(host, timeout);
  333. } catch (error) {
  334. return; // Ignore calls to dismiss the dialog if there is no dialog.
  335. }
  336. const node = host.getElementsByClassName('jp-Dialog')[0];
  337. if (node) {
  338. simulate(node as HTMLElement, 'keydown', { keyCode: 27 });
  339. }
  340. }
  341. /**
  342. * A namespace for private data.
  343. */
  344. namespace Private {
  345. let manager: ServiceManager;
  346. export const textFactory = new TextModelFactory();
  347. export const notebookFactory = new NotebookModelFactory({
  348. disableDocumentWideUndoRedo: false
  349. });
  350. /**
  351. * Get or create the service manager singleton.
  352. */
  353. export function getManager(): ServiceManager {
  354. if (!manager) {
  355. manager = new ServiceManager({ standby: 'never' });
  356. }
  357. return manager;
  358. }
  359. }