Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async vast loading #45

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"presets": [["env", {"targets": {"node": 4}}]],
"presets": ["es2015", "flow", "stage-3"],
"plugins": [
["transform-builtin-extend", {"globals": ["Error"]}],
"transform-runtime"
Expand Down
10 changes: 10 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[ignore]
.*/node_modules/documentation/.*
.*/test/.*

[include]

[libs]

[options]
unsafe.enable_getters_and_setters=true
236 changes: 147 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,137 +2,195 @@

[![npm](https://img.shields.io/npm/v/iab-vast-loader.svg)](https://www.npmjs.com/package/iab-vast-loader) [![Dependencies](https://img.shields.io/david/zentrick/iab-vast-loader.svg)](https://david-dm.org/zentrick/iab-vast-loader) [![Build Status](https://img.shields.io/circleci/project/github/zentrick/iab-vast-loader/master.svg)](https://circleci.com/gh/zentrick/iab-vast-loader) [![Coverage Status](https://img.shields.io/coveralls/zentrick/iab-vast-loader/master.svg)](https://coveralls.io/r/zentrick/iab-vast-loader) [![JavaScript Standard Style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)

Loads and parses IAB VAST tags, resolving wrapped tags along the way.
Loads IAB VAST tag trees using a preorder depth first strategy. The package is statically typed using [Flow](https://flow.org). [Observable streams](http://npmjs.com/package/rxjs) are used to update the consumer in time with new VAST documents.

This is a major rewrite from the [earlier version](https://github.com/zentrick/iab-vast-loader/tree/v0.8.0) of this package with the main benefit that it asynchronously fetches the complete VAST document tree. The previous version of this package used [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) and waited for the complete VAST tree to be fetched. One failing VAST document within the tree, made it fail completely. Cancellation semantics were also absent, by using Observables, we get them for free.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not have the readme assume that people are familiar with previous versions, or at least not by default. We should move most of this to a section on upgrading.


The new implementation gives you a `loadVast()` function that returns an Observable which allows you to react on failures and offers you the choice to continue listening for subsequent VAST documents in the tree. It also delivers you a newly VAST document right away (preserving preorder depth first traversal semantics), instead of waiting for the whole tree to be fetched.

It also gives you a `vastToAd()` function to map the a stream of VAST objects to a stream of Ad objects (both Wrapper and InLine), also in preorder depth first order. This gives you the right abstraction on which you can easily build further upon, using the RxJS `filter()` operator to for example only return `InLine` elements.

## Usage

```js
import VASTLoader from 'iab-vast-loader'
import { loadVast } from 'iab-vast-loader'

const tagUrl = 'https://example.com/vast.xml'

// Create the loader
const loader = new VASTLoader(tagUrl)
const vast$ = loadVast({
url: 'https://example.com/vast.xml'
})

// Load the tag chain and await the resulting Promise
loader.load()
.then((chain) => {
console.info('Loaded VAST tags:', chain)
// Load the VAST tree and log all the VAST tags in the tree.
vast$
.subscribe({
next: action => {
switch (action.type) {
case 'VAST_LOADED':
console.info('Loaded next VAST tag: ', action.vast)
break;
case 'VAST_LOADING_FAILED':
console.info('Loading next VAST tag failed: ', action.error, action.wrapper)
break;
}
},
complete: () => {
console.info('Finished loading the complete VAST tree')
}
})
.catch((err) => {
console.error('Error loading tag:', err)

// When interested in Ad events
const ad$ = vastToAd(vast$)

ad$
.subscribe({
next: action => {
switch (action.type) {
case 'AD_LOADED':
console.info('Loaded next Ad: ', action.ad)
break;
case 'AD_LOADING_FAILED'
console.info('Loading next Ad failed: ', action.error, action.wrapper)
break;
}
},
complete: () => {
console.info('Finished loading the complete VAST tree')
}
})
```

## API

### `#loadVast()`

```js
new VASTLoader(tagUrl[, options])
type LoadVast = (config: Config) => Observable<VastLoadAction>
```

Creates a VAST loader.
`loadVast` creates a stream of `VastLoadAction` objects. In a fully reactive codebase, this stream will be composed within another stream. If this library is used at the boundary, then you need to subscribe yourself like this:

```js
loader.load()
import { loadVast } from 'iab-vast-loader'
const vast$ = loadVast(config)

loadVast$.subscribe({
next: value => { },
complete: () => { }
})
```

Returns a `Promise` for an array of `VAST` instances. The `VAST` class is
provided by [iab-vast-model](https://www.npmjs.com/package/iab-vast-model).
#### Configuration

## Error Handling

In addition to the default export `VASTLoader`, the main module also exports
the `VASTLoaderError` class, which maps errors to the VAST specification:
`loadVast` accepts the following `Config` object:

```js
import { default as VASTLoader, VASTLoaderError } from 'iab-vast-loader'

const loader = new VASTLoader(tagUrl)
type Config = {
url: string,
maxDepth?: number,
timeout?: number,
retryCount?: number,
credentials: Credentials
}
```

loader.load()
.catch((err) => {
if (err instanceof VASTLoaderError) {
console.error('VAST error: ' + err.code + ' ' + err.message)
} else {
console.error('Unknown error: ' + err)
}
An overview of its properties:

- `url`: The url that points to the root VAST document of the VAST document tree that we need to fetch.
- `maxDepth`: The maximum number of VAST documents to load within one chain. The default is
`10`.
- `timeout`: The maximum number of milliseconds to spend per HTTP request. The default is
`10000`.
- `retryCount`: The amount of times it will retry fetching a VAST document in case of failure. The default is `0`.
- `credentials: Credentials`: Controls [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
behavior. You should pass an array of CredentialsType or functions that return a [CredentialsType value]((https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)), which is either `'omit'`, `'same-origin'` or `'include'`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if typing (or the flow types below) is the best way to convey the API for credentials. Something like "either an array containing any of ['omit', 'same-origin', 'include'], or a function that maps the request URL to one of those values"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, maybe we want to support a single string as well?


```js
type Credentials = (CredentialsType | (url: string) => CredentialsType)[]
type CredentialsType = 'omit' | 'same-origin' | 'include'
```

You can use this option to control the behavior on a per-request basis. For example:

```js
const loadVast$ = loadVast({
url: 'https://example.com/vast.xml',
credentials: [
url => uri.indexOf('.doubleclick.net/') !== 0 ? 'include' : 'omit'
]
})
```
```

As with [iab-vast-model](https://www.npmjs.com/package/iab-vast-model), if
`instanceof` doesn't work for you, you may want to inspect `error.$type`
instead. This issue can occur if you load multiple versions of iab-vast-loader,
each with their own `VASTLoaderError` class.
You can also pass multiple CORS strategies with the array. The implementation will race the different strategies in parallel, and will use the first request that succeeds. If none of the CORS strategies succeed, it will result in a `VAST_LOADING_FAILED` action. Notice that passing an empty array doesn't make sense, because it will make your request to fail always.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "because ..." at the end is kind of weird.


## Options
The default is `['omit']`.

### `maxDepth`
#### Output

The maximum number of VAST documents to load within one chain. The default is
10.
The output of loadVast is a stream of VastLoadAction objects:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put identifiers in backticks.


### `timeout`
```js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again not sure if I'd put flow types in the readme, but I'm not sure how else to do it. I don't want to scare people away just because they don't know about static typing though, because that shouldn't prevent them from using this library.

type VastLoadedAction = {
type: 'VAST_LOADED',
vast: VAST
}

type VastLoadingFailedAction = {
type: 'VAST_LOADING_FAILED',
error: VASTLoaderError,
wrapper: ?Wrapper
}

type VastLoadAction = VastLoadedAction | VastLoadingFailedAction
```

The `VAST` and `Wrapper` types are provided by [iab-vast-model](https://www.npmjs.com/package/iab-vast-model).

The maximum number of milliseconds to spend per HTTP request. The default is
10,000.
### `#vastToAd()`

### `credentials`
```js
import { loadVast, vastToAd } from 'iab-vast-loader'
const vast$ = loadVast(config)
const ad$ = vastToAd(vast$)
```

Controls [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
behavior. You can either pass a string or a function.
`vastToAd` maps a stream of `VastLoadAction` objects to `AdLoadAction` objects. You can subscribe on this stream directly, or indirectly using RxJS operators, just like `loadVast`.

If you pass a string, it will be used as the value for the `credentials` option
to every request.
[Valid values](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
are `'omit'` (the default), `'same-origin'` and `'include'`.
#### Output

To control the behavior on a per-request basis, pass a function receiving the
request URL and returning one of the accepted values. For example:
The output of `vastToAd` is a stream of `AdLoadAction` objects:

```js
const loader = new VASTLoader(wrapperUrl, {
credentials (uri) {
if (uri.indexOf('.doubleclick.net/') >= 0) {
return 'include'
} else {
return 'omit'
}
}
})
type AdLoadedAction = {
type: 'AD_LOADED',
ad: Ad
}

type AdLoadingFailedAction = {
type: 'AD_LOADING_FAILED',
error: VASTLoaderError,
wrapper: ?Wrapper
}

type AdLoadAction = AdLoadedAction | AdLoadingFailedAction
```

## Events
The `Ad` and `Wrapper` types are provided by [iab-vast-model](https://www.npmjs.com/package/iab-vast-model).

A `VASTLoader` is an `EventEmitter`. To be notified about progress, you can
subscribe to the events `willFetch`, `didFetch`, `willParse`, and `didParse`
as follows:
## Error Handling

```js
loader
.on('willFetch', ({ uri }) => {
console.info('Fetching', uri)
})
.on('didFetch', ({ uri, body }) => {
console.info('Fetched', body.length, 'bytes from', uri)
})
.on('willParse', ({ uri, body }) => {
console.info('Parsing', uri)
})
.on('didParse', ({ uri, body, vast }) => {
console.info('Parsed', uri)
})
.load()
.then((chain) => {
console.info('Loaded VAST tags:', chain)
})
.catch((err) => {
console.error('Error loading tag:', err)
})
```
In case the libary fails to load the next VAST document, it will emit a `VAST_LOADING_FAILED` action. You can react to this, by unsubscribing using the `takeUntil` operator, or you can continue listening for other values of another subtree of the VAST document tree. We don't push the stream into error state, because we want to enable the consumer to use subsequent VAST documents from the tree, after one subtree failed to fetch.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The further I read, the more I think we need some diagrams. That might also alleviate some of the typing overhead. :-)


In addition to the default export `VASTLoader`, the main module also exports
the `VASTLoaderError` class, which maps errors to the VAST specification. You can get the VAST error code using its `code` property, and the cause using its `cause` property.

As with [iab-vast-model](https://www.npmjs.com/package/iab-vast-model), if
`instanceof` doesn't work for you, you may want to inspect `error.$type`
instead. This issue can occur if you load multiple versions of iab-vast-loader,
each with their own `VASTLoaderError` class.

## Maintainer
## Maintainers

[Tim De Pauw](https://github.com/timdp)
- [Tim De Pauw](https://github.com/timdp)
- [Laurent De Smet](https://github.com/laurentdesmet)

## License

Expand Down
Loading