poll.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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. * #### Notes
  272. * The returned promise resolves after the tick is scheduled, but before
  273. * the polling action is run. To wait until after the poll action executes,
  274. * await the `poll.tick` promise: `await poll.refresh(); await poll.tick;`
  275. */
  276. refresh(): Promise<void> {
  277. return this.schedule({
  278. cancel: last => last.phase === 'refreshed',
  279. interval: Private.IMMEDIATE,
  280. phase: 'refreshed'
  281. });
  282. }
  283. /**
  284. * Starts the poll. Schedules `started` tick if necessary.
  285. *
  286. * @returns A promise that resolves after tick is scheduled and never rejects.
  287. */
  288. start(): Promise<void> {
  289. return this.schedule({
  290. cancel: last => last.phase !== 'standby' && last.phase !== 'stopped',
  291. interval: Private.IMMEDIATE,
  292. phase: 'started'
  293. });
  294. }
  295. /**
  296. * Stops the poll. Schedules `stopped` tick if necessary.
  297. *
  298. * @returns A promise that resolves after tick is scheduled and never rejects.
  299. */
  300. stop(): Promise<void> {
  301. return this.schedule({
  302. cancel: last => last.phase === 'stopped',
  303. interval: Private.NEVER,
  304. phase: 'stopped'
  305. });
  306. }
  307. /**
  308. * Schedule the next poll tick.
  309. *
  310. * @param next - The next poll state data to schedule. Defaults to standby.
  311. *
  312. * @param next.cancel - Cancels state transition if function returns `true`.
  313. *
  314. * @returns A promise that resolves when the next poll state is active.
  315. *
  316. * #### Notes
  317. * This method is protected to allow sub-classes to implement methods that can
  318. * schedule poll ticks.
  319. */
  320. protected async schedule(
  321. next: Partial<IPoll.State & { cancel: (last: IPoll.State) => boolean }> = {}
  322. ): Promise<void> {
  323. if (this.isDisposed) {
  324. return;
  325. }
  326. // The `when` promise in the constructor options acts as a gate.
  327. if (this.state.phase === 'constructed') {
  328. if (next.phase !== 'when-rejected' && next.phase !== 'when-resolved') {
  329. await this.tick;
  330. }
  331. }
  332. // Check if the phase transition should be canceled.
  333. if (next.cancel && next.cancel(this.state)) {
  334. return;
  335. }
  336. // Update poll state.
  337. const last = this.state;
  338. const pending = this._tick;
  339. const scheduled = new PromiseDelegate<this>();
  340. const state: IPoll.State<T, U> = {
  341. interval: this.frequency.interval,
  342. payload: null,
  343. phase: 'standby',
  344. timestamp: new Date().getTime(),
  345. ...next
  346. };
  347. this._state = state;
  348. this._tick = scheduled;
  349. // Clear the schedule if possible.
  350. if (last.interval === Private.IMMEDIATE) {
  351. cancelAnimationFrame(this._timeout);
  352. } else {
  353. clearTimeout(this._timeout);
  354. }
  355. // Emit ticked signal, resolve pending promise, and await its settlement.
  356. this._ticked.emit(this.state);
  357. pending.resolve(this);
  358. await pending.promise;
  359. // Schedule next execution and cache its timeout handle.
  360. const execute = () => {
  361. if (this.isDisposed || this.tick !== scheduled.promise) {
  362. return;
  363. }
  364. this._execute();
  365. };
  366. this._timeout =
  367. state.interval === Private.IMMEDIATE
  368. ? requestAnimationFrame(execute)
  369. : state.interval === Private.NEVER
  370. ? -1
  371. : setTimeout(execute, state.interval);
  372. }
  373. /**
  374. * Execute a new poll factory promise or stand by if necessary.
  375. */
  376. private _execute(): void {
  377. let standby =
  378. typeof this.standby === 'function' ? this.standby() : this.standby;
  379. standby =
  380. standby === 'never'
  381. ? false
  382. : standby === 'when-hidden'
  383. ? !!(typeof document !== 'undefined' && document && document.hidden)
  384. : true;
  385. // If in standby mode schedule next tick without calling the factory.
  386. if (standby) {
  387. void this.schedule();
  388. return;
  389. }
  390. const pending = this.tick;
  391. this._factory(this.state)
  392. .then((resolved: T) => {
  393. if (this.isDisposed || this.tick !== pending) {
  394. return;
  395. }
  396. void this.schedule({
  397. payload: resolved,
  398. phase: this.state.phase === 'rejected' ? 'reconnected' : 'resolved'
  399. });
  400. })
  401. .catch((rejected: U) => {
  402. if (this.isDisposed || this.tick !== pending) {
  403. return;
  404. }
  405. void this.schedule({
  406. interval: Private.sleep(this.frequency, this.state),
  407. payload: rejected,
  408. phase: 'rejected'
  409. });
  410. });
  411. }
  412. private _disposed = new Signal<this, void>(this);
  413. private _factory: Poll.Factory<T, U>;
  414. private _frequency: IPoll.Frequency;
  415. private _standby: Poll.Standby | (() => boolean | Poll.Standby);
  416. private _state: IPoll.State<T, U>;
  417. private _tick = new PromiseDelegate<this>();
  418. private _ticked = new Signal<this, IPoll.State<T, U>>(this);
  419. private _timeout = -1;
  420. }
  421. /**
  422. * A namespace for `Poll` types, interfaces, and statics.
  423. */
  424. export namespace Poll {
  425. /**
  426. * A promise factory that returns an individual poll request.
  427. *
  428. * @typeparam T - The resolved type of the factory's promises.
  429. *
  430. * @typeparam U - The rejected type of the factory's promises.
  431. */
  432. export type Factory<T, U> = (state: IPoll.State<T, U>) => Promise<T>;
  433. /**
  434. * Indicates when the poll switches to standby.
  435. */
  436. export type Standby = 'never' | 'when-hidden';
  437. /**
  438. * Instantiation options for polls.
  439. *
  440. * @typeparam T - The resolved type of the factory's promises.
  441. * Defaults to `any`.
  442. *
  443. * @typeparam U - The rejected type of the factory's promises.
  444. * Defaults to `any`.
  445. */
  446. export interface IOptions<T = any, U = any> {
  447. /**
  448. * A factory function that is passed a poll tick and returns a poll promise.
  449. */
  450. factory: Factory<T, U>;
  451. /**
  452. * The polling frequency parameters.
  453. */
  454. frequency?: Partial<IPoll.Frequency>;
  455. /**
  456. * The name of the poll.
  457. * Defaults to `'unknown'`.
  458. */
  459. name?: string;
  460. /**
  461. * Indicates when the poll switches to standby or a function that returns
  462. * a boolean or a `Poll.Standby` value to indicate whether to stand by.
  463. * Defaults to `'when-hidden'`.
  464. *
  465. * #### Notes
  466. * If a function is passed in, for any given context, it should be
  467. * idempotent and safe to call multiple times. It will be called before each
  468. * tick execution, but may be called by clients as well.
  469. */
  470. standby?: Standby | (() => boolean | Standby);
  471. /**
  472. * If set, a promise which must resolve (or reject) before polling begins.
  473. */
  474. when?: Promise<any>;
  475. }
  476. /**
  477. * Delays are 32-bit integers in many browsers so intervals need to be capped.
  478. *
  479. * #### Notes
  480. * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value
  481. */
  482. export const MAX_INTERVAL = 2147483647;
  483. }
  484. /**
  485. * A namespace for private module data.
  486. */
  487. namespace Private {
  488. /**
  489. * An interval value that indicates the poll should tick immediately.
  490. */
  491. export const IMMEDIATE = 0;
  492. /**
  493. * An interval value that indicates the poll should never tick.
  494. */
  495. export const NEVER = Infinity;
  496. /**
  497. * The default backoff growth rate if `backoff` is `true`.
  498. */
  499. export const DEFAULT_BACKOFF = 3;
  500. /**
  501. * The default polling frequency.
  502. */
  503. export const DEFAULT_FREQUENCY: IPoll.Frequency = {
  504. backoff: true,
  505. interval: 1000,
  506. max: 30 * 1000
  507. };
  508. /**
  509. * The default poll name.
  510. */
  511. export const DEFAULT_NAME = 'unknown';
  512. /**
  513. * The default poll standby behavior.
  514. */
  515. export const DEFAULT_STANDBY: Poll.Standby = 'when-hidden';
  516. /**
  517. * The first poll tick state's default values superseded in constructor.
  518. */
  519. export const DEFAULT_STATE: IPoll.State = {
  520. interval: NEVER,
  521. payload: null,
  522. phase: 'constructed',
  523. timestamp: new Date(0).getTime()
  524. };
  525. /**
  526. * The disposed tick state values.
  527. */
  528. export const DISPOSED_STATE: IPoll.State = {
  529. interval: NEVER,
  530. payload: null,
  531. phase: 'disposed',
  532. timestamp: new Date(0).getTime()
  533. };
  534. /**
  535. * Get a random integer between min and max, inclusive of both.
  536. *
  537. * #### Notes
  538. * From
  539. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#Getting_a_random_integer_between_two_values_inclusive
  540. *
  541. * From the MDN page: It might be tempting to use Math.round() to accomplish
  542. * that, but doing so would cause your random numbers to follow a non-uniform
  543. * distribution, which may not be acceptable for your needs.
  544. */
  545. function getRandomIntInclusive(min: number, max: number) {
  546. min = Math.ceil(min);
  547. max = Math.floor(max);
  548. return Math.floor(Math.random() * (max - min + 1)) + min;
  549. }
  550. /**
  551. * Returns the number of milliseconds to sleep before the next tick.
  552. *
  553. * @param frequency - The poll's base frequency.
  554. * @param last - The poll's last tick.
  555. */
  556. export function sleep(frequency: IPoll.Frequency, last: IPoll.State): number {
  557. const { backoff, interval, max } = frequency;
  558. const growth =
  559. backoff === true ? DEFAULT_BACKOFF : backoff === false ? 1 : backoff;
  560. const random = getRandomIntInclusive(interval, last.interval * growth);
  561. return Math.min(max, random);
  562. }
  563. }