poll.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { JSONExt, PromiseDelegate } from '@phosphor/coreutils';
  4. import { IDisposable } from '@phosphor/disposable';
  5. import { ISignal, Signal } from '@phosphor/signaling';
  6. /**
  7. * A readonly poll that calls an asynchronous function with each tick.
  8. *
  9. * @typeparam T - The resolved type of the factory's promises.
  10. * Defaults to `any`.
  11. *
  12. * @typeparam U - The rejected type of the factory's promises.
  13. * Defaults to `any`.
  14. */
  15. export interface IPoll<T = any, U = any> {
  16. /**
  17. * A signal emitted when the poll is disposed.
  18. */
  19. readonly disposed: ISignal<this, void>;
  20. /**
  21. * The polling frequency data.
  22. */
  23. readonly frequency: IPoll.Frequency;
  24. /**
  25. * Whether the poll is disposed.
  26. */
  27. readonly isDisposed: boolean;
  28. /**
  29. * The name of the poll.
  30. */
  31. readonly name: string;
  32. /**
  33. * The poll state, which is the content of the currently-scheduled poll tick.
  34. */
  35. readonly state: IPoll.State<T, U>;
  36. /**
  37. * A promise that resolves when the currently-scheduled tick completes.
  38. *
  39. * #### Notes
  40. * Usually this will resolve after `state.interval` milliseconds from
  41. * `state.timestamp`. It can resolve earlier if the user starts or refreshes the
  42. * poll, etc.
  43. */
  44. readonly tick: Promise<IPoll<T, U>>;
  45. /**
  46. * A signal emitted when the poll state changes, i.e., a new tick is scheduled.
  47. */
  48. readonly ticked: ISignal<IPoll<T, U>, IPoll.State<T, U>>;
  49. }
  50. /**
  51. * A namespace for `IPoll` types.
  52. */
  53. export namespace IPoll {
  54. /**
  55. * The polling frequency parameters.
  56. *
  57. * #### Notes
  58. * We implement the "decorrelated jitter" strategy from
  59. * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/.
  60. * Essentially, if consecutive retries are needed, we choose an integer:
  61. * `sleep = min(max, rand(interval, backoff * sleep))`
  62. * This ensures that the poll is never less than `interval`, and nicely
  63. * spreads out retries for consecutive tries. Over time, if (interval < max),
  64. * the random number will be above `max` about (1 - 1/backoff) of the time
  65. * (sleeping the `max`), and the rest of the time the sleep will be a random
  66. * number below `max`, decorrelating our trigger time from other pollers.
  67. */
  68. export type Frequency = {
  69. /**
  70. * Whether poll frequency backs off (boolean) or the backoff growth rate
  71. * (float > 1).
  72. *
  73. * #### Notes
  74. * If `true`, the default backoff growth rate is `3`.
  75. */
  76. readonly backoff: boolean | number;
  77. /**
  78. * The basic polling interval in milliseconds (integer).
  79. */
  80. readonly interval: number;
  81. /**
  82. * The maximum milliseconds (integer) between poll requests.
  83. */
  84. readonly max: number;
  85. };
  86. /**
  87. * The phase of the poll when the current tick was scheduled.
  88. */
  89. export type Phase =
  90. | 'constructed'
  91. | 'disposed'
  92. | 'reconnected'
  93. | 'refreshed'
  94. | 'rejected'
  95. | 'resolved'
  96. | 'standby'
  97. | 'started'
  98. | 'stopped'
  99. | 'when-rejected'
  100. | 'when-resolved';
  101. /**
  102. * Definition of poll state at any given time.
  103. *
  104. * @typeparam T - The resolved type of the factory's promises.
  105. * Defaults to `any`.
  106. *
  107. * @typeparam U - The rejected type of the factory's promises.
  108. * Defaults to `any`.
  109. */
  110. export type State<T = any, U = any> = {
  111. /**
  112. * The number of milliseconds until the current tick resolves.
  113. */
  114. readonly interval: number;
  115. /**
  116. * The payload of the last poll resolution or rejection.
  117. *
  118. * #### Notes
  119. * The payload is `null` unless the `phase` is `'reconnected`, `'resolved'`,
  120. * or `'rejected'`. Its type is `T` for resolutions and `U` for rejections.
  121. */
  122. readonly payload: T | U | null;
  123. /**
  124. * The current poll phase.
  125. */
  126. readonly phase: Phase;
  127. /**
  128. * The timestamp for when this tick was scheduled.
  129. */
  130. readonly timestamp: number;
  131. };
  132. }
  133. /**
  134. * A class that wraps an asynchronous function to poll at a regular interval
  135. * with exponential increases to the interval length if the poll fails.
  136. *
  137. * @typeparam T - The resolved type of the factory's promises.
  138. * Defaults to `any`.
  139. *
  140. * @typeparam U - The rejected type of the factory's promises.
  141. * Defaults to `any`.
  142. */
  143. export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
  144. /**
  145. * Instantiate a new poll with exponential backoff in case of failure.
  146. *
  147. * @param options - The poll instantiation options.
  148. */
  149. constructor(options: Poll.IOptions<T, U>) {
  150. this._factory = options.factory;
  151. this._standby = options.standby || Private.DEFAULT_STANDBY;
  152. this._state = { ...Private.DEFAULT_STATE, timestamp: new Date().getTime() };
  153. this.frequency = {
  154. ...Private.DEFAULT_FREQUENCY,
  155. ...(options.frequency || {})
  156. };
  157. this.name = options.name || Private.DEFAULT_NAME;
  158. // Schedule poll ticks after `when` promise is settled.
  159. (options.when || Promise.resolve())
  160. .then(_ => {
  161. if (this.isDisposed) {
  162. return;
  163. }
  164. return this.schedule({
  165. interval: Private.IMMEDIATE,
  166. phase: 'when-resolved'
  167. });
  168. })
  169. .catch(reason => {
  170. if (this.isDisposed) {
  171. return;
  172. }
  173. console.warn(`Poll (${this.name}) started despite rejection.`, reason);
  174. return this.schedule({
  175. interval: Private.IMMEDIATE,
  176. phase: 'when-rejected'
  177. });
  178. });
  179. }
  180. /**
  181. * The name of the poll.
  182. */
  183. readonly name: string;
  184. /**
  185. * A signal emitted when the poll is disposed.
  186. */
  187. get disposed(): ISignal<this, void> {
  188. return this._disposed;
  189. }
  190. /**
  191. * The polling frequency parameters.
  192. */
  193. get frequency(): IPoll.Frequency {
  194. return this._frequency;
  195. }
  196. set frequency(frequency: IPoll.Frequency) {
  197. if (this.isDisposed || JSONExt.deepEqual(frequency, this.frequency || {})) {
  198. return;
  199. }
  200. let { backoff, interval, max } = frequency;
  201. interval = Math.round(interval);
  202. max = Math.round(max);
  203. if (typeof backoff === 'number' && backoff < 1) {
  204. throw new Error('Poll backoff growth factor must be at least 1');
  205. }
  206. if (interval < 0 || interval > max) {
  207. throw new Error('Poll interval must be between 0 and max');
  208. }
  209. if (max > Poll.MAX_INTERVAL) {
  210. throw new Error(`Max interval must be less than ${Poll.MAX_INTERVAL}`);
  211. }
  212. this._frequency = { backoff, interval, max };
  213. }
  214. /**
  215. * Whether the poll is disposed.
  216. */
  217. get isDisposed(): boolean {
  218. return this.state.phase === 'disposed';
  219. }
  220. /**
  221. * Indicates when the poll switches to standby.
  222. */
  223. get standby(): Poll.Standby | (() => boolean | Poll.Standby) {
  224. return this._standby;
  225. }
  226. set standby(standby: Poll.Standby | (() => boolean | Poll.Standby)) {
  227. if (this.isDisposed || this.standby === standby) {
  228. return;
  229. }
  230. this._standby = standby;
  231. }
  232. /**
  233. * The poll state, which is the content of the current poll tick.
  234. */
  235. get state(): IPoll.State<T, U> {
  236. return this._state;
  237. }
  238. /**
  239. * A promise that resolves when the poll next ticks.
  240. */
  241. get tick(): Promise<this> {
  242. return this._tick.promise;
  243. }
  244. /**
  245. * A signal emitted when the poll ticks and fires off a new request.
  246. */
  247. get ticked(): ISignal<this, IPoll.State<T, U>> {
  248. return this._ticked;
  249. }
  250. /**
  251. * Dispose the poll.
  252. */
  253. dispose(): void {
  254. if (this.isDisposed) {
  255. return;
  256. }
  257. this._state = {
  258. ...Private.DISPOSED_STATE,
  259. timestamp: new Date().getTime()
  260. };
  261. this._tick.promise.catch(_ => undefined);
  262. this._tick.reject(new Error(`Poll (${this.name}) is disposed.`));
  263. this._disposed.emit();
  264. Signal.clearData(this);
  265. }
  266. /**
  267. * Refreshes the poll. Schedules `refreshed` tick if necessary.
  268. *
  269. * @returns A promise that resolves after tick is scheduled and never rejects.
  270. */
  271. refresh(): Promise<void> {
  272. return this.schedule({
  273. cancel: last => last.phase === 'refreshed',
  274. interval: Private.IMMEDIATE,
  275. phase: 'refreshed'
  276. });
  277. }
  278. /**
  279. * Starts the poll. Schedules `started` tick if necessary.
  280. *
  281. * @returns A promise that resolves after tick is scheduled and never rejects.
  282. */
  283. start(): Promise<void> {
  284. return this.schedule({
  285. cancel: last => last.phase !== 'standby' && last.phase !== 'stopped',
  286. interval: Private.IMMEDIATE,
  287. phase: 'started'
  288. });
  289. }
  290. /**
  291. * Stops the poll. Schedules `stopped` tick if necessary.
  292. *
  293. * @returns A promise that resolves after tick is scheduled and never rejects.
  294. */
  295. stop(): Promise<void> {
  296. return this.schedule({
  297. cancel: last => last.phase === 'stopped',
  298. interval: Private.NEVER,
  299. phase: 'stopped'
  300. });
  301. }
  302. /**
  303. * Schedule the next poll tick.
  304. *
  305. * @param next - The next poll state data to schedule. Defaults to standby.
  306. *
  307. * @param next.cancel - Cancels state transition if function returns `true`.
  308. *
  309. * @returns A promise that resolves when the next poll state is active.
  310. *
  311. * #### Notes
  312. * This method is protected to allow sub-classes to implement methods that can
  313. * schedule poll ticks.
  314. */
  315. protected async schedule(
  316. next: Partial<
  317. IPoll.State & { cancel: ((last: IPoll.State) => boolean) }
  318. > = {}
  319. ): Promise<void> {
  320. if (this.isDisposed) {
  321. return;
  322. }
  323. const pending = this._tick;
  324. // The `when` promise in the constructor options acts as a gate.
  325. if (this.state.phase === 'constructed') {
  326. if (next.phase !== 'when-rejected' && next.phase !== 'when-resolved') {
  327. await pending.promise;
  328. }
  329. }
  330. // Check if the phase transition should be canceled.
  331. if (next.cancel && next.cancel(this.state)) {
  332. return;
  333. }
  334. // Update poll state.
  335. const last = this.state;
  336. const scheduled = new PromiseDelegate<this>();
  337. const state: IPoll.State<T, U> = {
  338. interval: this.frequency.interval,
  339. payload: null,
  340. phase: 'standby',
  341. timestamp: new Date().getTime(),
  342. ...next
  343. };
  344. this._state = state;
  345. this._tick = scheduled;
  346. // Clear the schedule if possible.
  347. if (last.interval === Private.IMMEDIATE) {
  348. cancelAnimationFrame(this._timeout);
  349. } else {
  350. clearTimeout(this._timeout);
  351. }
  352. // Emit ticked signal, resolve pending promise, and await its settlement.
  353. this._ticked.emit(this.state);
  354. pending.resolve(this);
  355. await pending.promise;
  356. // Schedule next execution and cache its timeout handle.
  357. const execute = () => {
  358. if (this.isDisposed || this.tick !== scheduled.promise) {
  359. return;
  360. }
  361. this._execute();
  362. };
  363. this._timeout =
  364. state.interval === Private.IMMEDIATE
  365. ? requestAnimationFrame(execute)
  366. : state.interval === Private.NEVER
  367. ? -1
  368. : setTimeout(execute, state.interval);
  369. }
  370. /**
  371. * Execute a new poll factory promise or stand by if necessary.
  372. */
  373. private _execute(): void {
  374. let standby =
  375. typeof this.standby === 'function' ? this.standby() : this.standby;
  376. standby =
  377. standby === 'never'
  378. ? false
  379. : standby === 'when-hidden'
  380. ? !!(typeof document !== 'undefined' && document && document.hidden)
  381. : true;
  382. // If in standby mode schedule next tick without calling the factory.
  383. if (standby) {
  384. void this.schedule();
  385. return;
  386. }
  387. const pending = this.tick;
  388. this._factory(this.state)
  389. .then((resolved: T) => {
  390. if (this.isDisposed || this.tick !== pending) {
  391. return;
  392. }
  393. void this.schedule({
  394. payload: resolved,
  395. phase: this.state.phase === 'rejected' ? 'reconnected' : 'resolved'
  396. });
  397. })
  398. .catch((rejected: U) => {
  399. if (this.isDisposed || this.tick !== pending) {
  400. return;
  401. }
  402. void this.schedule({
  403. interval: Private.sleep(this.frequency, this.state),
  404. payload: rejected,
  405. phase: 'rejected'
  406. });
  407. });
  408. }
  409. private _disposed = new Signal<this, void>(this);
  410. private _factory: Poll.Factory<T, U>;
  411. private _frequency: IPoll.Frequency;
  412. private _standby: Poll.Standby | (() => boolean | Poll.Standby);
  413. private _state: IPoll.State<T, U>;
  414. private _tick = new PromiseDelegate<this>();
  415. private _ticked = new Signal<this, IPoll.State<T, U>>(this);
  416. private _timeout = -1;
  417. }
  418. /**
  419. * A namespace for `Poll` types, interfaces, and statics.
  420. */
  421. export namespace Poll {
  422. /**
  423. * A promise factory that returns an individual poll request.
  424. *
  425. * @typeparam T - The resolved type of the factory's promises.
  426. *
  427. * @typeparam U - The rejected type of the factory's promises.
  428. */
  429. export type Factory<T, U> = (state: IPoll.State<T, U>) => Promise<T>;
  430. /**
  431. * Indicates when the poll switches to standby.
  432. */
  433. export type Standby = 'never' | 'when-hidden';
  434. /**
  435. * Instantiation options for polls.
  436. *
  437. * @typeparam T - The resolved type of the factory's promises.
  438. * Defaults to `any`.
  439. *
  440. * @typeparam U - The rejected type of the factory's promises.
  441. * Defaults to `any`.
  442. */
  443. export interface IOptions<T = any, U = any> {
  444. /**
  445. * A factory function that is passed a poll tick and returns a poll promise.
  446. */
  447. factory: Factory<T, U>;
  448. /**
  449. * The polling frequency parameters.
  450. */
  451. frequency?: Partial<IPoll.Frequency>;
  452. /**
  453. * The name of the poll.
  454. * Defaults to `'unknown'`.
  455. */
  456. name?: string;
  457. /**
  458. * Indicates when the poll switches to standby or a function that returns
  459. * a boolean or a `Poll.Standby` value to indicate whether to stand by.
  460. * Defaults to `'when-hidden'`.
  461. *
  462. * #### Notes
  463. * If a function is passed in, for any given context, it should be
  464. * idempotent and safe to call multiple times. It will be called before each
  465. * tick execution, but may be called by clients as well.
  466. */
  467. standby?: Standby | (() => boolean | Standby);
  468. /**
  469. * If set, a promise which must resolve (or reject) before polling begins.
  470. */
  471. when?: Promise<any>;
  472. }
  473. /**
  474. * Delays are 32-bit integers in many browsers so intervals need to be capped.
  475. *
  476. * #### Notes
  477. * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value
  478. */
  479. export const MAX_INTERVAL = 2147483647;
  480. }
  481. /**
  482. * A namespace for private module data.
  483. */
  484. namespace Private {
  485. /**
  486. * An interval value that indicates the poll should tick immediately.
  487. */
  488. export const IMMEDIATE = 0;
  489. /**
  490. * An interval value that indicates the poll should never tick.
  491. */
  492. export const NEVER = Infinity;
  493. /**
  494. * The default backoff growth rate if `backoff` is `true`.
  495. */
  496. export const DEFAULT_BACKOFF = 3;
  497. /**
  498. * The default polling frequency.
  499. */
  500. export const DEFAULT_FREQUENCY: IPoll.Frequency = {
  501. backoff: true,
  502. interval: 1000,
  503. max: 30 * 1000
  504. };
  505. /**
  506. * The default poll name.
  507. */
  508. export const DEFAULT_NAME = 'unknown';
  509. /**
  510. * The default poll standby behavior.
  511. */
  512. export const DEFAULT_STANDBY: Poll.Standby = 'when-hidden';
  513. /**
  514. * The first poll tick state's default values superseded in constructor.
  515. */
  516. export const DEFAULT_STATE: IPoll.State = {
  517. interval: NEVER,
  518. payload: null,
  519. phase: 'constructed',
  520. timestamp: new Date(0).getTime()
  521. };
  522. /**
  523. * The disposed tick state values.
  524. */
  525. export const DISPOSED_STATE: IPoll.State = {
  526. interval: NEVER,
  527. payload: null,
  528. phase: 'disposed',
  529. timestamp: new Date(0).getTime()
  530. };
  531. /**
  532. * Get a random integer between min and max, inclusive of both.
  533. *
  534. * #### Notes
  535. * From
  536. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#Getting_a_random_integer_between_two_values_inclusive
  537. *
  538. * From the MDN page: It might be tempting to use Math.round() to accomplish
  539. * that, but doing so would cause your random numbers to follow a non-uniform
  540. * distribution, which may not be acceptable for your needs.
  541. */
  542. function getRandomIntInclusive(min: number, max: number) {
  543. min = Math.ceil(min);
  544. max = Math.floor(max);
  545. return Math.floor(Math.random() * (max - min + 1)) + min;
  546. }
  547. /**
  548. * Returns the number of milliseconds to sleep before the next tick.
  549. *
  550. * @param frequency - The poll's base frequency.
  551. * @param last - The poll's last tick.
  552. */
  553. export function sleep(frequency: IPoll.Frequency, last: IPoll.State): number {
  554. const { backoff, interval, max } = frequency;
  555. const growth =
  556. backoff === true ? DEFAULT_BACKOFF : backoff === false ? 1 : backoff;
  557. const random = getRandomIntInclusive(interval, last.interval * growth);
  558. return Math.min(max, random);
  559. }
  560. }