Skip to content

Commit

Permalink
✨ PollingObserver now extends EventTarget
Browse files Browse the repository at this point in the history
Other changes:

* Event handler can be setup by listening to `finish event,
  e.g. `observer.addEventListener('finish', ...)`
* Update demo with usage and API references
* Add test for custom event
  • Loading branch information
motss committed May 22, 2019
1 parent cb051d5 commit 3813c46
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 17 deletions.
239 changes: 226 additions & 13 deletions polling_observer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,261 @@

[![MIT License][mit-license-badge]][mit-license-url]

> Simple [deno] module to remove any accents/ diacritics found in a string.
> Like PerformanceObserver or any other observer APIs you could find in a browser, but this is for polling. Not only does it run polling with defined parameters but also collect polling metrics for each run until timeout or a defined condition fulfills.
## Table of contents <!-- omit in toc -->

- [Usage](#usage)
- [API Reference](#api-reference)
- [OnfinishFulfilled&lt;T&gt;](#onfinishfulfilledlttgt)
- [OnfinishRejected](#onfinishrejected)
- [PollingMeasure](#pollingmeasure)
- [Methods](#methods)
- [PollingMeasure.toJSON()](#pollingmeasuretojson)
- [PollingObserver&lt;T&gt;](#pollingobserverlttgt)
- [Methods](#methods-1)
- [PollingObserver.observe(callback[, options])](#pollingobserverobservecallback-options)
- [PollingObserver.disconnect()](#pollingobserverdisconnect)
- [PollingObserver.takeRecords()](#pollingobservertakerecords)
- [Event handler](#event-handler)
- [PollingObserver.onfinish](#pollingobserveronfinish)
- [Events](#events)
- [finish](#finish)
- [License](#license)

## Usage

```ts
/** Import from GH via `denopkg` */
import { PollingObserver } from "https://denopkg.com/motss/[email protected]/polling_observer/mod.ts";
interface DataType {
status: 'complete' | 'in-progress';
items: Record<string, any>[];
}

(async () => {
})();
import { PollingObserver } from 'https://denopkg.com/motss/[email protected]/polling_observer/mod.ts';

const obs = new PollingObserver((data/** list, observer */) => {
const { status, items } = data || {};
const itemsLen = (items && items.length) || 0;

/** Stop polling when any of the conditions fulfills */
return 'complete' === status || itemsLen > 99;
});

/**
* When polling finishes, it will either fulfill or reject depending on the status:
*
* | Status | Returns |
* | ------- | --------- |
* | finish | <value> |
* | timeout | <value> |
* | error | <reason> |
*
* Alternatively, `obs.addEventListener('finish', ...);` works too.
*/
obs.onfinish = (data, records/**, observer */) => {
const { status, value, reason } = data || {};

switch (status) {
case 'error': {
console.error(`Polling fails due to: `, reason);
break;
}
case 'timeout': {
console.log(`Polling timeouts after 30 seconds: `, value);
break;
}
case 'finish':
default: {
console.log(`Polling finishes: `, value);
}
}

console.log(`Formatted polling records: `, records.map(n => n.toJSON()));
/**
* [
* {
* duration: 100,
* entryType: 'polling-measure',
* name: 'polling:0',
* startTime: 100,
* },
* ...
* ]
*/

obs.disconnect(); /** Disconnect to clean up */
};

obs.observe(
async () => {
/** Polling callback - fetch resources */
const r = await fetch('https://example.com/api?key=123');
const d = await r.json();

return d;
},
/** Run polling (at least) every 2 seconds and timeout if it exceeds 30 seconds */
{
interval: 2e3,
timeout: 30e3,
}
);
```

## API Reference

### OnfinishFulfilled&lt;T&gt;

```ts
interface OnfinishFulfilled<T> {
status: 'finish' | 'timeout';
value: T;
}
```

### OnfinishRejected

```ts
interface OnfinishRejected {
status: 'error';
reason: Error;
}
```

### PollingMeasure

```ts
interface PollingMeasure {
duration: number;
entryType: 'polling-measure';
name: string;
startTime: number;
}
```

- `duration` <[number][number-mdn-url]> Duration of the polling takes in milliseconds.
- `entryType` <[string][string-mdn-url]> Entry type, defaults to `polling-measure`.
- `name` <[string][string-mdn-url]> Polling name in the format of `polling:<index>` where `<index>` starts from `0` and increments on each polling.
- `startTime` <[string][string-mdn-url]> Relative timestamp (in milliseconds ) indicates when the polling starts at.

#### Methods

##### PollingMeasure.toJSON()

- <[Function][function-mdn-url]> Returns a JSON representation of the polling object's properties.

---

### PollingObserver&lt;T&gt;

- `conditionCallback` <[Function][function-mdn-url]> Condition callback to be executed in each polling and return the condition result in the type of boolean, e.g. return `true` to stop next poll.
- `data` <`T`> Polling data returned by `callback` in the type of `T` which defined in the [PollingObserver.observe()] method.
- `entries` <[Array][array-mdn-url]&lt;[PollingMeasure]&gt;> A list of [PollingMeasure] objects.
- `observer` <[PollingObserver]&lt;`T`&gt;> Created [PollingObserver] object.
- returns: <[boolean][boolean-mdn-url]> If `true`, the polling stops. Returning `false` will result in an infinite polling as the condition will never meet.
- returns: <[PollingObserver]&lt;`T`&gt;> [PollingObserver] object.

#### Methods

##### PollingObserver.observe(callback[, options])

The method is used to initiate polling with a polling callback and optional configuration.

- `callback` <[Function][function-mdn-url]> Callback to be executed in each polling and return the result so that it will be passed as the first argument in `conditionCallback`.
- returns: <`T` | [Promise][promise-mdn-url]&lt;`T`&gt;> Return polling result in the type of `T` or `Promise<T>` in each polling.
- `options` <[Object][object-mdn-url]> Optional configuration to run the polling.
- `interval` <[number][number-mdn-url]> Optional interval in milliseconds. This is the minimum delay before starting the next polling.
- `timeout` <[number][number-mdn-url]> Optional timeout in milliseconds. Polling ends when it reaches the defined timeout even though the condition has not been met yet. _As long as `timeout` is not a number or it has a value that is less than 1, it indicates an infinite polling. The polling needs to be stopped manually by calling [PollingObserver.disconnect()] method._

##### PollingObserver.disconnect()

Once a `PollingObserver` disconnects, the polling stops and all polling metrics will be cleared. Calling [PollingObserver.takeRecords()] after the disconnection will always return an empty record.

A `onfinish` event handler can be used to retrieve polling records after a disconnection but it has to be attached before disconnecting the observer.

##### PollingObserver.takeRecords()

The method returns a list of [PollingMeasure] object containing the metrics of each polling.

- returns: <[Array][array-mdn-url]&lt;[PollingMeasure]&gt;> A list of [PollingMeasure] objects.

#### Event handler

##### PollingObserver.onfinish

_Alternatively, an event handler can be setup to listen for the `finish` event. See [finish]._

Event handler for when a polling finishes. When a polling finishes, it can either be fulfilled with a `value` or rejected with a `reason`. Any one of which contains a `status` field to tell the state of the finished polling.

- `onfinishCallback` <[Function][function-mdn-url]> Callback to be executed when a polling finishes.
- `data` <[OnfinishFulfilled&lt;T&gt;]|[OnfinishRejected]> When a polling fulfills, it returns an [OnfinishFulfilled&lt;T&gt;] object with `status` set to `finish` or `timeout` and a `value` in the type of `T`. Whereas a polling rejects, it returns an [OnfinishRejected] object with `status` set to `error` and a `reason` in the type of [Error][error-mdn-url].

| Status | Returns |
| --------- | -------------- |
| `finish` | &lt;value&gt; |
| `timeout` | &lt;value&gt; |
| `error` | &lt;reason&gt; |

- `entries` <[Array][array-mdn-url]&lt;[PollingMeasure]&gt;> A list of [PollingMeasure] objects.
- `observer` <[PollingObserver]&lt;`T`&gt;> Created [PollingObserver] object.

#### Events

##### finish

`finish` event fires when a polling finishes.

```ts
const obs = new PollingObserver(/** --snip */);
// --snip

/** Same as using obs.onfinish = ... */
obs.addEventListener('finish', (ev: CustomEvent) => {
const {
detail: [
{ status, value/**, reason */ },
records,
observer,
],
} = ev;

// --snip
});
```

## License

[MIT License](http://motss.mit-license.org/) © Rong Sen Ng

<!-- References -->

[deno]: https://github.com/denoland/deno

<!-- MDN -->
[PollingObserver]: #pollingobservert
[PollingObserver.observe()]: #pollingobserverobservecallback-options
[PollingObserver.disconnect()]: #pollingobserverdisconnect
[PollingObserver.takeRecords()]: #pollingobservertakerecords
[PollingMeasure]: #pollingmeasure
[PollingMeasure.toJSON()]: #pollingmeasuretojson
[OnfinishFulfilled&lt;T&gt;]: #onfinishfulfilledt
[OnfinishRejected]: #onfinishrejected
[finish]: #finish

<!-- MDN -->
[array-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
[boolean-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[function-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
[map-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
[boolean-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[html-style-element-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
[promise-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[regexp-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[set-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
[void-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void
[error-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error

<!-- Badges -->

[mit-license-badge]: https://flat.badgen.net/badge/license/MIT/blue

<!-- Links -->

[mit-license-url]: https://github.com/motss/deno_mod/blob/master/LICENSE
15 changes: 11 additions & 4 deletions polling_observer/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ function isPromise<T>(r: T | Promise<T>): r is Promise<T> {
return 'function' === typeof((r as Promise<T>).then);
}

export class PollingObserver<T> {
export class PollingObserver<T> extends EventTarget {
public onfinish?: OnfinishCallback<T>;

private _forceStop: boolean = false;
private _records: PollingMeasure[] = [];
private _isPolling: boolean = false;

constructor(public conditionCallback: ConditionCallback<T>) {
super();

if ('function' !== typeof(conditionCallback)) {
throw new TypeError(`'conditionCallback' is not defined`);
}
Expand Down Expand Up @@ -116,9 +118,14 @@ export class PollingObserver<T> {
/** NOTE(motss): Reset flags */
this._isPolling = this._forceStop = false;

if ('function' === typeof(onfinishCallback)) {
onfinishCallback(result, recordsSlice, this);
}
if ('function' === typeof(onfinishCallback)) onfinishCallback(result, recordsSlice, this);

this.dispatchEvent(new CustomEvent('finish', {
bubbles: true,
cancelable: true,
composed: true,
detail: [result, recordsSlice, this],
}))
}
}

Expand Down
29 changes: 29 additions & 0 deletions polling_observer/mod_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,34 @@ async function willPollWithoutOnfinishCallback() {
assertEquals((await task).length > 1, true);
}

async function willFireFinishEvent() {
const data: MockData = { items: [Math.floor(Math.random() * Math.PI)] };
const obs = new PollingObserver<MockData>(async () => false);
const task = new Promise<CustomEvent>((yay) => {
obs.addEventListener(
'finish',
// (ev: CustomEvent<[OnfinishFulfilled<MockData>, PollingMeasure[]]>) => yay(ev.detail)
(ev: CustomEvent) => yay(ev)
);
});

obs.observe(() => data, { interval: 2e3, timeout: 5e3 });

const {
detail: [
{ status, value },
records,
],
// [OnfinishFulfilled<MockData>, PollingMeasure[]]
} = await task;


assertStrictEq(status, "timeout");
assertEquals(value, { ...data });
assertStrictEq(records.length > 1, true);
assertStrictEq(obs.takeRecords().length > 1, true);
}

prepareTest([
failsWhenConditionCallbackIsUndefined,
failsWhenErrorOccurs,
Expand All @@ -393,4 +421,5 @@ prepareTest([
willPollWithAsyncConditionCallback,
willPollWithSyncCallback,
willPollWithoutOnfinishCallback,
willFireFinishEvent,
], "polling_observer");

0 comments on commit 3813c46

Please sign in to comment.