master a5d9c55362d3 cached
136 files
277.6 KB
82.2k tokens
545 symbols
1 requests
Download .txt
Showing preview only (312K chars total). Download the full file or copy to clipboard to get everything.
Repository: matthew-andrews/workshop-making-it-work-offline
Branch: master
Commit: a5d9c55362d3
Files: 136
Total size: 277.6 KB

Directory structure:
gitextract_2g5j4x9v/

├── .gitignore
├── .jshintrc
├── 01-introduction/
│   ├── README.md
│   ├── dysfunctional-family.md
│   ├── how.md
│   └── why.md
├── 02-new-apis/
│   ├── README.md
│   ├── fetch.md
│   └── solutions.md
├── 03-offline-todo/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 02-opening-a-database/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 03-using-dev-tools/
│   │   └── README.md
│   ├── 04-creating-object-stores/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 05-review-window-indexeddb/
│   │   └── README.md
│   ├── 06-adding-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 07-getting-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 08-rendering-todos/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 09-deleting-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 10-review-requests-transactions/
│   │   └── README.md
│   ├── 11-appcache/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 12-appcache-gotcha-1/
│   │   └── README.md
│   ├── 13-success/
│   │   └── README.md
│   └── README.md
├── 04-offline-todo-with-sync/
│   ├── 01-architecture/
│   │   └── README.md
│   ├── 02-mark-for-deletion/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 03-adding-ajax/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── fetch.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 04-synchronize/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── fetch.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 05-success/
│   │   └── README.md
│   └── README.md
├── 05-offline-news/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 02-single-multi-page/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 03-hacking-appcache/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── iframe.html
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 04-more-hacking-appcache/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── appcache.js
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── iframe.html
│   │       ├── iframe.js
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 05-success/
│   │   └── README.md
│   └── README.md
├── 06-offline-news-with-service-worker/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 02-registering-a-service-worker/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   ├── application.js
│   │   │   ├── service-worker.js
│   │   │   ├── styles.css
│   │   │   └── templates.js
│   │   ├── styles.css
│   │   └── templates.js
│   ├── 03-service-worker-caches/
│   │   └── README.md
│   ├── 04-success/
│   │   └── README.md
│   └── README.md
├── 07-dexie/
│   └── README.md
├── 08-success/
│   └── README.md
├── README.md
└── package.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/node_modules/


================================================
FILE: .jshintrc
================================================
{
	"browser": true,
	"laxbreak": true,
	"debug": true
}


================================================
FILE: 01-introduction/README.md
================================================
# Introduction

Today we're going to explore all the technologies available in browsers today that can be brought together to build **offline websites**.

## Me

- Core dev FT web app, Economist HTML5 app (both successful offline-first applications)
- Author of tutorial series: [How to make an offline HTML5 web app, FT style](http://labs.ft.com/2012/08/basic-offline-html5-web-app/)
- [Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL on Smashing Magazine](http://www.smashingmagazine.com/2014/09/02/building-simple-cross-browser-offline-todo-list-indexeddb-websql/)

## You

- Introduction yourselves
- How would you describe your JavaScript skills?
- What do you hope to gain from today? (Mostly to learn about techniques to use today, or one that will be usable in a few month's time?)

## Course structure

The structure of this course will be very practical and hands-on focused on building prototypes, and will typically follow this pattern:

- Hack something together that works.
- Talk about why it works (explaining any browser APIs that we've used that may be unfamiliar).
- Discuss why it's inefficient or inelegant and then improve on it.
- There are exercises at the end of many sections if you finish the core material quickly.
- Schedule for the day: 9 start, 10.30 break (½ hour), 12.30 lunch (1 hour), 15.00 break (½ hour), 17.00 the end.

## What you should get out of today's course

- A few working prototypes.
- All the prototypes are on my GitHub (publicly so if you have any problems with any after today - please feel free to raise issues there and I will try to find time to help)

## Group chat

http://tlk.io/

---

[← Back to *contents*](https://github.com/matthew-andrews/workshop-making-it-work-offline) | [Continue to *why* →](./why.md)


================================================
FILE: 01-introduction/dysfunctional-family.md
================================================
# Meet the dysfunctional family

## Quiz

- Can you name all the offline storage technologies that exist in browsers today?
- What are the differences between them?
- What sort of data can you store offline?
- Which technologies allow you to shared data between domain names?
- What are the rules for offline technologies that don't share data across domains?  How is https affected this?  How about subfolders?
- If you built an offline web application and then migrated to https what potential problems might you need to consider?

## Choices

- Cookies
- Local Storage / Session Storage
- WebSQL
- IndexedDB
- Browser Cache
- Application Cache
- Service Worker

### Cookies

- Basically work on *every* browser
- Only supports very small pieces of information
- Only supports strings
- Sent on every http request
- A bit fiddly to use without a helper library/function

### Local Storage

- Works on a lot of browsers
- Supports more data than cookies
- But still only a small amount of space available on most browsers
- Very *fast*
- Only supports strings
- Not sent on HTTP request (only available client side)
- Synchronous, which can cause performance issues

### Local database (WebSQL, IndexedDB)

- One or the other (or both) work on most modern browsers
- Supports more data than local storage (typically around ~50mb, but can be much bigger on Desktop)
- Supports complex data structures and multiple data types
- Built in mechanism for migrations
- Slower than Local Storage
- Not sent on http request (only available client side)
- Asynchronous

### Browser Cache

- Works on every browser
- *Sometimes* does work offline (which can be confusing when trying to test AppCache issues) but…
- It can't be relied on

### Application Cache

- Works on most (90%+) modern browsers
- Best suited for storing application *code*
- The only option to reliably load a website from nothing offline
- Can storage a significant amount of data (50mb+)
- Very difficult use without causing unintended consequences for most websites

### Service Worker

- Application Cache's replacement, but unlikely to be possible to polyfill
- Not available in any browser (bits of it work on some cutting-edge browsers with feature flags switched on)
- Safari / Microsoft have not announced any intention to implement it
- Will only work on https
- Promises to be less difficult to use and cause fewer side effects than Application Cache

Note. There was another storage technology relevant to offline websites called the HTML5 FileSystem API but as of April 2014, [that spec is dead](http://www.html5rocks.com/en/tutorials/file/filesystem/)

#### Q. Which technology do you need to make an offline-first web application?

A. Most, if not all, of them.

For today's browsers I advocate the following approach and the reasons why will hopefully become clear over the course of the workshop:

### Guidance

- **Delivering the application** - we will use the Application Cache to store the JavaScript, HTML and CSS to launch our application.  We will not store any content here.
- **Storing content** - we will use a clientside database technology, either IndexedDB or WebSQL, to store our content.  We will not store any application assets here.

(Note: when we are able to use Service Worker we will be able to take a more flexible approach)

---

[← Back to *how*](./how.md) | [Continue to *a promising start* →](../02-new-apis)


================================================
FILE: 01-introduction/how.md
================================================
# How

There are four distinct problems to solve*:

1. Delivering the application
2. Storing data
3. Syncing data
4. Third parties

## Delivering the application

- How do we efficiently cache  enough of the pieces that make up a website for it to be useful offline?
- How do we guarantee that the browser won't delete them?

## Storing content

- How do we store content downloaded to or created on users' devices?

## Synchronizing content

- How do we ensure the data on users' devices are kept in sync with content on the server?
- What if there are conflicts?

## Third parties

One of the great things about the web is how easy it is to bring multiple products, widgets and services together onto a single page.

Offline technologies have only just started to be developed - using them enable us to create incredible user experience that previously would have been impossible on the web - but, also by using them, you will discover many of the third party components you relied on to build websites don't work well offline.

### What happens to Google Analytics data?

Basically, nothing.

- For each tracking event Google Analytics will try to send a tracking event to the server.
- As the device is offline that tracking event will fail.
- Google Analytics will not retry or store that data locally for retry later.
- **If you want to keep that data, you will have to implement the storing and resending of it yourself.**
- Even, even if you _did_ store that data and sent it later manually yourself, as the web requires your website to be open in order to make http requests that data might only be sent after a very long time after it was recorded - _perhaps even weeks or months_.

\* [Adapted from Caolan McMahon's presentation on the status of offline web](http://www.infoq.com/presentations/status-web-offline)

[← Back to *why*](./why.md) | [Continue to *meet the dysfunctional family* →](./dysfunctional-family.md)


================================================
FILE: 01-introduction/why.md
================================================
# Why

We live in a bubble.  We work all day on high-spec machines, plugged into high-speed connections and use the latest cut of our favourite web browser.  The real world isn't like that.

Taking just network connection as one example.  It's no longer as simple as online-versus-offline.

- Connected to a high speed fibre connection via wifi every few minutes for less than a minute (the London underground)
- In a region where the cost of using the local network is so high the user has _chosen_ not to be connected.
- Connected to a weak wireless signal where only a small number of requests are successful.
- Using a connection where only a subset of websites are blocked (an airport or in China - it's possible to make your website nearly unusable in China by poorly integration of a Facebook widget)
- Device prefer to connect to wifi hotspots that require logins, even when they have perfectly good mobile connections.

## People need the web offline

Whilst the web is slowly reaching ever more remote locations - wifi on planes and underground - the amount by which we depend on it is growing even faster.

As developers we need to do a better job.

[← Back to *introduction*](./README.md) | [Continue to *how* →](./how.md)


================================================
FILE: 02-new-apis/README.md
================================================
# Promises

## What is a Promise?

> The Promise interface **represents a proxy for a value not necessarily known when the promise is created**. It allows you to associate handlers to an asynchronous action's eventual success or failure. **This lets asynchronous methods return values like synchronous methods**: instead of the final value, the asynchronous method returns a promise of having a value at some point in the future.
>
> A pending promise can become either **fulfilled with a value**, or **rejected with a reason**. When either of these happens, the associated handlers queued up by a promise's then method are called. (If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.)
>
> As the `Promise.prototype.then` and `Promise.prototype.catch` methods return promises, they can be chained—an operation called composition.

\- Source: [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), emphasis mine.

In short, Promises are a way to organise asynchronous logic.

## Lots of Promise libraries

Or similar libraries that achieve the same thing:-

- Q
- async
- when
- RSVP

But the implementation we're going to use is **ES6 Promise**.  It's a browser standard that is already available and also works in Node.

## Why are we talking about it?

We're going to need Promises because new browser APIs, such as **Service Worker** which we will cover later, are built on top of them.  By covering them early we will be able to use them in all of our prototypes and exercises so that hopefully they will be second nature (if they aren't already) by the end of the day.	

Take for example messy logic that **POSTs** two todos to the server and opens an alert when both have succeeded (or an alert if one fails).

```js
request({
  url: "https://offline-todo-api.herokuapp.com/todos",
  body: {
    _id: 'my-first-todo',
    text: 'Buy some bread'
  },
  method: 'POST'
}, function(err) {
  var firstSuccess = !!err;
  request({
    url: "https://offline-todo-api.herokuapp.com/todos",
    body: {
      _id: 'my-second-todo',
      text: 'Buy some bread'
    },
    method: 'POST'
  }, function(err) {
    var secondSuccess = !!err;
    if (firstSuccess && secondSuccess) {
      alert('success!');
    } else {
      alert('something failed');
    }
  });
});
```

## What's wrong with this code?

- Difficult to follow
- The second request doesn't start until the first is complete (they could be running at the same time)
- Error handling is messy

## What are we aiming for?

If we didn't need to worry about the asynchronous ajax request we'd probably write something like this:-

```js
try {
  var responses = [];
  responses[0] = request({
    url: "https://offline-todo-api.herokuapp.com/todos",
    body: {
      _id: 'my-first-todo',
      text: 'Buy some bread'
    },
    method: 'POST'
  });
  responses[1] = request({
    url: "https://offline-todo-api.herokuapp.com/todos",
    body: {
      _id: 'my-second-todo',
      text: 'Buy some bread'
    },
    method: 'POST'
  });
  alert('success!');
} catch(e) {
  alert('something failed');
}
```

### Promises let us write code like this.

```js
Promise.all(
  [
    request({
      url: "https://offline-todo-api.herokuapp.com/todos",
      body: {
        _id: 'my-first-todo',
        text: 'Buy some bread'
      },
      method: 'POST'
    }),
    request({
      url: "https://offline-todo-api.herokuapp.com/todos",
      body: {
        _id: 'my-second-todo',
        text: 'Buy some bread'
      },
      method: 'POST'
    })
  ]
  ).then(function success(responses) {
    alert('success!');
  }, function failure() {
    alert('something failed');
  });
```

### Nice things about promises

- Abstracts the complexity of dealing with asynchronous logic - lets you write performant but (relatively) simple code.
- Allows you to deal with errors in asynchronous libraries in a sensible way.

### Maybe not so nice things about promises

- Most browser methods were made pre-promises and so need wrapping to be useful.
- Cannot pipe data.  (eg. it might be nice for ajax request that returned a lot of data to start passing that data to the application as soon as data starts being received.  With promises you must wait until *all the data arrives* before your application will get to see *any of it*.

### Like them or not they're here to stay

- Browsers now support them natively, Node v0.11 ships with them built in
- Old familiar APIs (e.g. XMLHttpRequest) may be replaced by new shiny promising versions (e.g. request)
- They are a fundamental building block to ServiceWorkers, which will be a key component of today's workshop
- Many, many languages support promises (even [ReactPHP](https://github.com/reactphp/promise)) or equivalents (sometimes called futures - promises were actually first proposed in 1976)

### Getting setup for Promises

```
mkdir promises
cd promises
echo '{}' >> package.json
npm install --save es6-promise
```

Then, in that folder create a file called `test.js` with the following contents:-

```js
require('es6-promise').polyfill();

console.log(Promise);
```

Hopefully the output will be:-

```
{ [Function: Promise]
  all: [Function: all],
  race: [Function: race],
  resolve: [Function: resolve],
  reject: [Function: reject] }
```

### Exercises

Create a function that returns a promise that successfully resolves after 1 second with `setTimeout`

---

Create a function that returns a promise that rejects a promise after half a second with `setTimeout`

---

Explain the difference between this:-

```js
promise
  .then(function() {
      alert("Success!");
    }, function() {
      alert("Fail!");
    });
```

And this:-

```js
promise
  .then(function() {
    alert("Success!");
  })
  .catch(function() {
    alert("Fail!");
  });
```

---

Explain the difference between this:-

```js
Promise.all([promise1, promise2])
  .then(function() {
    alert("Success!");
  });
```

And this:-

```js
Promise.race([promise1, promise2])
  .then(function() {
    alert("Success!");
  });
```

---

When might race be useful in an offline web application?

---

[← Back to *meet the dysfunctional family*](../01-introduction/dysfunctional-family.md) | [Continue to *Fetch* →](./fetch.md)


================================================
FILE: 02-new-apis/fetch.md
================================================
# Fetch

By now I hope I've convinced you that ES6 Promises are the one and only solution to asynchronous flow and that — if you're anything like me — will no longer be able to look at a web api built in the Pre-Promise era without cringing.

Let's talk about ajax.

```js
function success(response) { /* do something */ }
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
  if (request.readyState === 4) {
    if (request.status === 200) {
      success(request.response);
    }
  }
};
request.open('https://offline-news-api.herokaupp.com/stories');
request.send();
```

OK, to be fair nobody actually writes this.  They'll normally do something like:-

```js
function success(response) { /* do something */ }
$.get('https://offline-news-api.herokaupp.com/stories', success);
```

Which is much better but what's the point of a standard API that is so difficult to use nobody uses it?

What would be nice if browsers shipped with a simple, _Promise_ based API for _fetching_ ajax requests…

```js
fetch('https://offline-news-api.herokaupp.com/stories')
  .then(success);
```

[Luckily other people think this is a good idea too](http://fetch.spec.whatwg.org/), and is the mechanism you *must* use within Service Worker, which we'll cover later.  Oddly though Chrome hasn't made this available in the front end, however the lovely people at [GitHub](https://www.github.com/github/fetch) have built a polyfill for modern browsers.

## Gotchas

The Fetch API builds on top of the W3C Streams _as well as_ Promises.  This is good as there are times where you won't want to wait for an ajax request to finish loading before you want to use the data it's downloading — in these cases you can pipe that raw stream of downloaded data into different bits of your application.  JavaScript streams are out of scope for this workshop but you can read more about them on the [Stream Handbook](https://github.com/substack/stream-handbook).

Warning: the Fetch Polyfill _only_ implements the Promise part of the API, *not* the Streams part.

The reason why I mention it is that the `fetch` API returns a Promise that resolves with a Response object that gives you access to *streams* — not an object containing all the data you requested.  You need to then convert that into the data format you want.  For example:-

```js
fetch('http://mattandre.ws/my-json-file.json')
  .then(function(response) {
    return response.json();
  });
```

## Exercises

- Use the Fetch API to create a, delete a and download all the todos from the API described: https://github.com/matthew-andrews/offline-todo-api.

---

[← Back to *Promises*](./) | [Continue to *offline todo with IndexedDB* →](../03-offline-todo)


================================================
FILE: 02-new-apis/solutions.md
================================================
# Solutions

```js
function successful() {
  return new Promise(resolve, reject) {
    setTimeout(resolve, 1000);
  };
}
```

---

```js
function unsuccessful() {
  return new Promise(resolve, reject) {
    setTimeout(reject, 500);
  };
}
```

---

`then` actually takes two arguments - one for success, one for failure, and `catch(function() {})` is just shorthand for `then(undefined, function() {})`.

So really we are comparing the difference between:-

```js
promise
  .then(function() {
      alert("Success!");
    }, function() {
      alert("Fail!");
    });
```

And this:-

```js
promise
  .then(function() {
    alert("Success!");
  })
  .then(undefined, function() {
    alert("Fail!");
  });
```

When an error occurs promises _jump forward_ to the next `then` with a rejection callback.  So with`.then(funcA, funcB)` either `funcA` or `funcB` will be called (not both) - whereas with `.then(funcA).catch(funcB)` both `funcA` and `funcB` will get called (as they're separate steps).

[Read more detail about error handling in promises on HTML5 rocks](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-error-handling)

---

- `all` resolves when **all** the promises passed into it resolve.  If any rejects, it immediately reject - discarding the results of all other promises, regardless of their result.
- `race` resolves **as soon as one** of the promises resolves or rejects.

Race could be useful in an offline web app when retrieving fresh data from a server that is also cached locally.

```js
Promise.race([getFromServer(), getFromCache()])
  .then(function() {
    // Put some data on screen
  });
```


================================================
FILE: 03-offline-todo/01-scaffolding/README.md
================================================
# Scaffolding the application

We will create the following files in a single directory:

- [`/index.html`](./index.html)
- [`/application.js`](./application.js)
- [`/indexeddb.shim.min.js`](./indexeddb.shim.min.js)
- [`/promise.js`](./promise.js)
- [`/styles.css`](./styles.css)
- [`/offline.appcache`](./offline.appcache)

##### `/index.html`

```html
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>
```

Nothing surprising here: just a standard HTML web page, with an input field to add to-do items, and an empty unordered list that will be filled with those items.

##### `/indexeddb.shim.min.js`

Download the contents of [the minified IndexedDB polyfill](https://raw.githubusercontent.com/matthew-andrews/offline-todo/gh-pages/indexeddb.shim.min.js), and put it in this file.

##### `/promise.js`

Download the contents of [the minified ES6 Promise polyfill](http://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js), and put it in this file.

##### `/styles.css`

```css
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}
```

Again, this should be quite familiar: just some simple styles to make the to-do list look tidy. You may choose not to
have any styles at all or create your own.

We will leave `application.js` and `offline.appcache` empty for now and return to them later.

## Quick test

Run any simple static web server, for example [node-static](https://github.com/cloudhead/node-static), in the directory
containing these files and verify the website matches the screenshot below.

![Screenshot of the scaffolded application](./screenshot.png)

Warning: you will need to use `static -c 1` rather than `static` on its own in order to see updated files.  Once we've added AppCache, try leaving off the `-c 1` and see what happens when you update files.

---

[← Back to *Offline todo with IndexedDB*](../) | [Continue to *opening a database* →](../02-opening-a-database)


================================================
FILE: 03-offline-todo/01-scaffolding/application.js
================================================


================================================
FILE: 03-offline-todo/01-scaffolding/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/01-scaffolding/offline.appcache
================================================



================================================
FILE: 03-offline-todo/01-scaffolding/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/01-scaffolding/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/02-opening-a-database/README.md
================================================
# Opening an IndexedDB database

##### `/application.js`

```js
(function() {
  var db;

  databaseOpen()
    .then(function() {
      alert("The database has been opened");
    });

  function databaseOpen() {
    return new Promise(function(resolve, reject) {
      var version = 1;
      var request = indexedDB.open('todos', version);
      request.onsuccess = function(e) {
        db = e.target.result;
        resolve();
      };
      request.onerror = reject;
    });
  }

}());
```

All this code does is create a database with `indexedDB.open` and then shows the user an old-fashioned alert if it is successful.  Every IndexedDB database needs a name (in this case, `todos`) and a version number (which I’ve set to 1).

But how can we check that this has actually worked?

---

[← Back to *scaffolding the application*](../01-scaffolding) | [Continue to *using dev tools* →](../03-using-dev-tools)


================================================
FILE: 03-offline-todo/02-opening-a-database/application.js
================================================
(function() {

	// 'global' variable to store reference to the database
	var db;

	databaseOpen()
		.then(function() {
			alert("The database has been opened");
		});

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 03-offline-todo/02-opening-a-database/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/02-opening-a-database/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/02-opening-a-database/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/03-using-dev-tools/README.md
================================================
# Using dev tools

### Exercise

- Find debug tools for viewing IndexedDB and WebSQL databases across all the major web browsers and check our database has been created across all of them.

### Chrome

To check that the database has been succesfully created open the application in the browser, open up **Developer Tools** and click on the **Resources** tab.

![Screenshot of the IndexedDB in Chrome Dev Tools](./chrome.jpg)

### Firefox

- Download [the IndexedDB browser](https://addons.mozilla.org/en-us/firefox/addon/indexeddb-browser/),
- Find your profile folder, `cd ~/Library/Application\ Support/Firefox/Profiles/1yj54vgo.default/`.
- Create a symbolic link: `ln -s ./storage/persistent/ ./indexedDB`

![Firefox IndexedDB Dev Tools](./firefox.png)

### Safari

Safari is also a little different because it does not yet support IndexedDB in stable.  You can however still verify that the polyfill has correctly set the database up in WebSQL.

![Safari IndexedDB Dev Tools](./safari.png)

---

[← Back to *opening a database*](../02-opening-a-database) | [Continue to *creating object stores* →](../04-creating-object-stores)


================================================
FILE: 03-offline-todo/04-creating-object-stores/README.md
================================================
# Creating object stores

Like many database formats that you might be familiar with, you can create many tables in a single IndexedDB database. These tables are called **objectStores**. In this step, we’ll create an object store named `todo`. To do this, we simply add an event listener on the database’s `upgradeneeded` event.
The data format that we will store to-do items in will be JavaScript objects, with two properties:

`_id` This timestamp will also act as our key.
`text` This is the text that the user has entered.

For example:
```
{ _id: 1407594483201, text: 'Wash the dishes' }
```

Now, `/application.js` looks like this (the new code starts at `request.onupgradeneeded`):

```js
(function() {

  // 'global' variable to store reference to the database
  var db;

  databaseOpen()
    .then(function() {
      alert("The database has been opened");
    });

  function databaseOpen() {
    return new Promise(function(resolve, reject) {
      var version = 1;
      var request = indexedDB.open('todos', version);

      // Run migrations if necessary
      request.onupgradeneeded = function(e) {
        db = e.target.result;
        e.target.transaction.onerror = reject;
        db.createObjectStore('todo', { keyPath: '_id' });
      };
  
      request.onsuccess = function(e) {
        db = e.target.result;
        resolve();
      };
      request.onerror = reject;
    });
  }
}());
```

This will create an object store keyed by `_id` and named `todo`.

## Or will it?

Having updated `application.js`, if you open the web app again, not a lot happens. The code in `onupgradeneeded` never runs; try adding a `console.log` in the `onupgradeneeded` callback to be sure. The problem is that we haven’t incremented the version number, so the browser doesn’t know that it needs to run the upgrade callback.

## How to solve this?

Whenever you add or remove object stores, you will need to increment the version number. Otherwise, the structure of the data will be different from what your code expects, and you risk breaking the application.
Because this application doesn’t have any real users yet, we can fix this another way: by deleting the database. Copy this line of code into the **Console**, and then refresh the page:

```js
indexedDB.deleteDatabase('todos');
```

After refreshing, the **Resources** pane of **Developer Tools** should have changed and should now show the object store that we added:
The Resources panel should now show the object store that was added.

![The “Resources” panel should now show the object store that was added](./screenshot.png)

Note: if Browser Dev Tools still down show the object store try closing and re-opening Dev Tools as sometimes Dev Tools don't refresh properly.

---

### Exercises

- What are the advantages and the disadvantages of this compared to other approaches to software migrations in software applications?
- Why might it be a good idea to minimize the number of database schema changes you make?

### Bonus Exercise

- Write a migration that effectively renames the object store from `todos` to `todoList` (in version 2), and another to `todoItem` (in version 3) and back to `todos` (in version 4).

---


[← Back to *Using Dev Tools*](../03-using-dev-tools) | [Continue to *Review: `window.indexedDB`* →](../05-review-window-indexeddb)


================================================
FILE: 03-offline-todo/04-creating-object-stores/application.js
================================================
(function() {

	// 'global' variable to store reference to the database
	var db;

	databaseOpen()
		.then(function() {
			alert("The database has been opened");
		});

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 03-offline-todo/04-creating-object-stores/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/04-creating-object-stores/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/04-creating-object-stores/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/05-review-window-indexeddb/README.md
================================================
## Review: `window.indexedDB`

Everything related to **IndexedDB** is accessed either directly or indirectly through `window.indexedDB`\*.

### Opening databases

In our application, we start by *requesting* an IndexedDB database named `todos` be opened.

```js
var request = indexedDB.open('todos', version);
```

#### Current syntax\**

```js
IDBOpenDBRequest open (DOMString name, [EnforceRange] optional unsigned long long version);
```

When `open` is called, if a database of this name already exists, the browser will give us that - if not, it will create a new one.  If the database is successfully opened a `success` event will be
fired on the `IDBOpenDBRequest` object.

The `version` parameter allows us to be able to change the schema of the database.  If we want to add or remove 'tables' (called Object Stores) we must implement the logic for doing that within
`onupgradeneeded`.  Note, the *only* time browser will let us add, remove or edit the structure of Object Stores is within the `onupgradeneeded` callback.

Note: `upgradeneeded` events will fire when the database is first created **as well as** when the version number has changed.  In addition to the 'just created' cas' if your database has changed more
than once (for example, if the current verson is 3) your `onupgradeneeded` event handler must be able to handle upgrading databases from version 1 to 3 as well as version 2 to 3.  **This can get
quite complicated for applications whose database schemas change frequently**.

```js
var request = window.indexedDB.open("todos", 2);

request.onupgradeneeded = function(e) {
  alert("Database upgrade needed");
};

request.onsuccess = function(e) {
  alert("Database opened successfully");
};
```

\* Documentation for this object is often filed under `IDBFactory`.  `IDBFactory` is an **interface** that `window.indexedDB` *implements* (and indeed the *only* object that implements this
interface).

\*\* May be subject to change, [see MDN](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory.open)

### Deleting Databases

Deleting IndexedDB databases is also done via the `window.indexedDB` object via the `window.indexedDB.deleteDatabase` method.

#### Current syntax\*

```js
IDBOpenDBRequest deleteDatabase (DOMString name);
```

#### Example

```js
var request = window.indexedDB.deleteDatabase("todos");
request.onsuccess = function(e) {
  alert("Database opened successfully");
};
```

\* Again, may be subject to change, [see MDN](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory.deleteDatabase).

---

[← Back to *creating object stores*](../04-creating-object-stores) | [Continue to *adding data* →](../06-adding-data)


================================================
FILE: 03-offline-todo/06-adding-data/README.md
================================================
# Adding items

The next step is to enable the user to add items.

##### `/application.js`

Note that I’ve omitted the database’s opening code, indicated by ellipses (…) below:

```js
(function() {
  var db, input;

  databaseOpen()
    .then(function() {
      input = document.querySelector('input');
      document.body.addEventListener('submit', onSubmit);
    });

  function onSubmit(e) {
    e.preventDefault();
    var todo = { text: input.value, _id: String(Date.now()) };
    databaseTodosPut(todo)
      .then(function() {
        input.value = '';
      });
  }

[…]

  function databaseTodosPut(todo) {
    return new Promise(function(resolve, reject) {
      var transaction = db.transaction(['todo'], 'readwrite');
      var store = transaction.objectStore('todo');
      var request = store.put(todo);
      transaction.oncomplete = resolve;
      request.onerror = reject;
    });
  }

}());
```

We’ve added two bits of code here:

- The event listener responds to every `submit` event, prevents that event’s default action (which would otherwise refresh the page), calls `databaseTodosPut` with the value of the `input` element, and (if the item is successfully added) sets the value of the `input` element to be empty.
- A function named `databaseTodosPut` returns a promise, stores the to-do item in the local database, along with a timestamp, and then resolves the promise.

To test that this works, open up the web app again. Type some words into the `input` element and press **Enter**. Repeat this a few times, and then open up **Developer Tools** to the **Resources** tab again. You should see the items that you typed now appear in the todo object store.

![After adding a few items, they should appear in the todo object store](./screenshot.png)

After adding a few items, they should appear in the todo object store.

#### Exercises

- Find out what the difference is between `IDBObjectStore#add` and `IDBObjectStore#put` is.  What are the advantages of using `put` over `add`?


#### Solutions

- It's equivalent to the difference between the `POST` and `PUT` verbs in RESTful APIs, or `INSERT` versus `REPLACE` in SQL.  `add` will always create something fresh and if an attempt is made to create something with a key that already exists it will fail - whereas `put` will override the item with the existing key.  We use `put` here to simplify the implementation for demonstration purposes (we don't need to handle the error case).

---

[← Back to *Review: `window.indexedDB`*](../05-review-window-indexeddb) | [Continue to *retrieving data* →](../07-getting-data)


================================================
FILE: 03-offline-todo/06-adding-data/application.js
================================================
(function() {
	var db, input;

	databaseOpen()
		.then(function() {
			input = document.querySelector('input');
			document.body.addEventListener('submit', onSubmit);
		});

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			});
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 03-offline-todo/06-adding-data/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/06-adding-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/06-adding-data/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/07-getting-data/README.md
================================================
# Retrieving items

Now that we’ve stored some data, the next step is to work out how to retrieve it.

##### `/application.js`

Again, the ellipses indicate code that we have already implemented in the previous steps.

```js
(function() {
  var db, input;

  databaseOpen()
    .then(function() {
      input = document.querySelector('input');
      document.body.addEventListener('submit', onSubmit);
    })
    .then(databaseTodosGet)
    .then(function(todos) {
      console.log(todos);
    }); 

[…]

  function databaseTodosGet() {
    return new Promise(function(resolve, reject) {
      var transaction = db.transaction(['todo'], 'readonly');
      var store = transaction.objectStore('todo');

      // Get everything in the store
      var keyRange = IDBKeyRange.lowerBound(0);
      var cursorRequest = store.openCursor(keyRange);

      // This fires once per row in the store, so for simplicity collect the data
      // in an array (data) and send it pass it in the resolve call in one go
      var data = [];
      cursorRequest.onsuccess = function(e) {
        var result = e.target.result;

        // If there's data, add it to array
        if (result) {
          data.push(result.value);
          result.continue();

        // Reach the end of the data
        } else {
          resolve(data);
        }
      };
    });
  }

}());
```

After the database has been initialized, this will retrieve all of the to-do items and output them to Dev Tools console.

Notice how the `onsuccess` callback is called after each item is retrieved from the object store. To keep things simple, we put each result into an array named `data`, and when we run out of results (which happens when we’ve retrieved all of the items), we resolve the promise with that array. This approach is simple, but other approaches might be more efficient.

If you reopen the application again, Dev Tools console should look a bit like this:

![Screenshot of the scaffolded application](./screenshot.png)

---

[← Back to *adding data*](../06-adding-data) | [Continue to *rendering todos* →](../08-rendering-todos)


================================================
FILE: 03-offline-todo/07-getting-data/application.js
================================================
(function() {
	var db, input;

	databaseOpen()
		.then(function() {
			input = document.querySelector('input');
			document.body.addEventListener('submit', onSubmit);
		})
		.then(databaseTodosGet)
		.then(function(todos) {
			console.log(todos);
		});

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			});
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet() {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					data.push(result.value);
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

}());


================================================
FILE: 03-offline-todo/07-getting-data/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/07-getting-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/07-getting-data/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/08-rendering-todos/README.md
================================================
# Rendering todos

In this section we will put in the plumbing that will use the methods we've already implemented to render the todos on to the page.

```js
(function() {
  var db, input, ul;

  databaseOpen()
    .then(function() {
      input = document.querySelector('input');
      ul = document.querySelector('ul');
      document.body.addEventListener('submit', onSubmit);
    })
    .then(refreshView);

  function onSubmit(e) {
    e.preventDefault();
    var todo = { text: input.value, _id: String(Date.now()) };
    databaseTodosPut(todo)
      .then(function() {
        input.value = '';
      })
      .then(refreshView);
  }

[…]

  function refreshView() {
    return databaseTodosGet().then(renderAllTodos);
  }

  function renderAllTodos(todos) {
    var html = '';
    todos.forEach(function(todo) {
      html += todoToHtml(todo);
    });
    ul.innerHTML = html;
  }

  function todoToHtml(todo) {
    return '<li>'+todo.text+'</li>';
  }

[…]
```

- `todoToHtml` takes a single todo and converts it to a HTML string.
- `renderAllTodos` takes an array of todos, converts each of them HTML and then sets the `innerHTML` of the `ul` to those strings concatenated together.
- `refreshView` returns a promise then gets all the todos from the local database and passes them to `renderAllTodos`, which converts the todos to HTML and injects that HTML into the web page.
- We have also enhanced another method, the `onSubmit` event handler, so that the todos re-render as new todos are added.

---

[← Back to *getting data*](../07-getting-data) | [Continue to *deleting data* →](../09-deleting-data)


================================================
FILE: 03-offline-todo/08-rendering-todos/application.js
================================================
(function() {
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.querySelector('input');
			ul = document.querySelector('ul');
			document.body.addEventListener('submit', onSubmit);
		})
		.then(refreshView);

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet().then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet() {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					data.push(result.value);
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

}());


================================================
FILE: 03-offline-todo/08-rendering-todos/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/08-rendering-todos/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/08-rendering-todos/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/09-deleting-data/README.md
================================================
# Deleting items

To keep things as simple as possible, we will let users delete items by clicking on a delete button next to each of them.

To achieve this, we will be a little hacky and give each item an ID set to its `_id` (which is actually a timestamp). This will enable the click event listener, which we will add to the document’s body, to detect when the user clicks on a delete button (as opposed to anywhere else on the page).

##### `/application.js`

```js
(function() {
  var db, input, ul;

  databaseOpen()
    .then(function() {
      input = document.querySelector('input');
      ul = document.querySelector('ul');
      document.body.addEventListener('submit', onSubmit);
      document.body.addEventListener('click', onClick);
    })
    .then(refreshView);

  function onClick(e) {

    // We'll assume that any element with an ID
    // attribute is a to-do item. Don't try this at home!
    e.preventDefault();
    if (e.target.hasAttribute('id')) {
      databaseTodosGetById(e.target.getAttribute('id'))
        .then(function(todo) {
          return databaseTodosDelete(todo);
        })
        .then(refreshView);
    }
  }

[…]

  function todoToHtml(todo) {
    return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
  }

[…]

  function databaseTodosGetById(id) {
    return new Promise(function(resolve, reject) {
      var transaction = db.transaction(['todo'], 'readwrite');
      var store = transaction.objectStore('todo');
      var request = store.get(id);
      request.onsuccess = function(e) {
        var result = e.target.result;
        resolve(result);
      };
      request.onerror = reject;
    });
  }

  function databaseTodosDelete(todo) {
    return new Promise(function(resolve, reject) {
      var transaction = db.transaction(['todo'], 'readwrite');
      var store = transaction.objectStore('todo');
      var request = store.delete(todo._id);
      transaction.oncomplete = resolve;
      request.onerror = reject;
    });
  }

}());
```

We’ve made the following enhancements:

- We’ve added a new event handler (`onClick`) that listens to click events and checks whether the target element has an ID attribute. If it has one it calls databaseTodosDelete with that value and, if the item is successfully deleted, re-renders the to-do list following the same approach that we took in step 6.
- We’ve enhanced the `todoToHtml` function so that every to-do item is outputted with a delete button with an ID attribute set to its `_id`.
- We’ve added two new database functions:
  - `databaseTodosDelete`, which takes a `todo` (well, a javascript object with an `_id` property) and returns a promise, deletes the item and then resolves the promise, and
  - `databaseTodosGetById`, which takes an `_id` and returns a promise, which resolves with the todo keyed by the given `_id`.

Our to-do app is basically feature-complete. We can add and delete items, and it works in any browser that supports WebSQL or IndexedDB (although it could be a lot more efficient).

---

[← Back to *rendering todos*](../08-rendering-todos) | [Continue to *Review: `IDBRequest` and `IDBTransaction`* →](../10-review-requests-transactions)


================================================
FILE: 03-offline-todo/09-deleting-data/application.js
================================================
(function() {
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.querySelector('input');
			ul = document.querySelector('ul');
			document.body.addEventListener('submit', onSubmit);
			document.body.addEventListener('click', onClick);
		})
		.then(refreshView);

	function onClick(e) {
		e.preventDefault();
		if (e.target.hasAttribute('id')) {
			databaseTodosGetById(e.target.getAttribute('id'))
				.then(function(todo) {
					return databaseTodosDelete(todo);
				})
				.then(refreshView);
		}
	}

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet().then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet() {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					data.push(result.value);
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseTodosGetById(id) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.get(id);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseTodosDelete(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.delete(todo._id);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 03-offline-todo/09-deleting-data/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/09-deleting-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/09-deleting-data/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/10-review-requests-transactions/README.md
================================================
# Review `IDBRequest` and `IDBTransaction`

Now that we've covered selecting, inserting and deleting data with IndexedDB let's review the functions we've used.

## Inserting data

```js
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
```

To insert a item into the object store we first need to create a **transaction**.  In IndexedDB all changes to the database, whether they be reads, updates, adds, deletions or changes to the structure all need to be wrapped in a transaction

You can do multiple reads, adds, updates, deletes or changes to the structure in a single transaction - but if any fail - the whole transaction fails none of the changes are applied.

You can only make changes to the structure of the database during a callback to the `upgradeneeded` event just after the database has been set up.

Transactions can only be created by calling `transaction` method on the `IDBDatabase` database.

### Transactions

```js
IDBTransaction transaction ((DOMString or sequence<DOMString>) storeNames, optional IDBTransactionMode mode = "readonly");
```
- `storeNames` - an array of object stores that your transaction needs to access.
- `mode` - there are two modes for transactions: `readwrite` or `readonly`.

#### Warning

Use `transaction.oncomplete` and not `request.onsuccess` as the point where you know the data has been stored.

### Object Stores

You can think of Object Stores as similar to tables in a SQL database.  You can have many object stores per database, and they can contain many objects - indexed by at least one index.  Object Stores are accessed via transaction objects.  For example:-

```js
var transaction = db.transaction['todo'], 'readonly');
var store = transaction.objectStore('todo');
```

### Requests

Requests are the only objects that you can use to _read_ or _write_ data with IndexedDB.  You can get to the results of each request using event handlers on the request objects.

Results are created by the methods on object stores such as `IDBObjectStore#add`, `IDBObjectStore#delete` , `IDBObjectStore#createIndex`, `IDBObjectStore#openCursor`, etc.  A special type of `IDBRequest` called `IDBOpenDBRequest` that we covered in the previous review is created by `IDBFactory#open` and `IDBFactory#deleteDatabase`.

## Deleting data

```js
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
```

We can now talk through each line and explain what it does:-

- Create an `IDBTransaction` object with `readwrite` access to the `todo` object store, assign it to `transaction`
- Get an `IDBStore` object that represents the object store `todo` and assign it to `store`
- Create a request to `delete` object with ID `todo._id` from `store`
- When complete, `resolve` the promise

## Getting data

```js
  var transaction = db.transaction(['todo'], 'readonly');
  var store = transaction.objectStore('todo');

  var keyRange = IDBKeyRange.lowerBound(0);
  var cursorRequest = store.openCursor(keyRange);

  var data = [];
  cursorRequest.onsuccess = function(e) {
    var result = e.target.result;

    if (result) {
      data.push(result.value);
      result.continue();
    } else {
      resolve(data);
    }
  };
```

- As we're only getting data we only need to create a `readonly` transaction.
- Then we get an `IDBStore` object that represents the object store `todo` and assigns it to `store` - in the same way deleting works above.
- The next two lines are concerned with creating a cursor (an object that allows us to walk through the database).  The line `IDBKeyRange.lowerBound(0)` simply says start from the beginning of the table.
- As data is pulled out of the table `success` events are fired on the cursor.  In each callback we must call `continue` to proceed to the next `result`, and if that `result` is empty we know we've reached the end of the dataset.

#### Cursors

[`IDBCursors`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor) can be used to look up data by their index as well as sort data within an object store.  Usage of them is normally out of scope for this course - but here are a few links that introduce the key ideas:

- [`IDBCursor` documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor)
- [How to do some magic with indexedDB](http://www.codeproject.com/Articles/744986/How-to-do-some-magic-with-indexedDB)

### Exercises

- Try changing `transaction.oncomplete` to `request.onsuccess` inside `databaseTodosPut` in your todo app.  What happens when you create todos?  Why?
- Reverse the order of the todos so that they added to the top, not the bottom, in our demo.
- What would be different if we'd have used `add` instead of `put`?  What are the benefits of one over the other?
- Aside from `cursorRequest.lowerBound` what other approaches could you take to retrieving all the data from a table?
- _Bonus:_ Write code that selects items from an object store that instead of waiting for all of them to come back before rendering those items onto the page, immediately render each one.  What approaches or browser APIs instead of Promises could be more appropriate to implement this?
- _Bonus:_ Support the ability to update existing items on our todo list.

---

[← Back to *deleting todos*](../09-deleting-todos) | [Continue to *truly offline* →](../11-appcache)


================================================
FILE: 03-offline-todo/11-appcache/README.md
================================================
# Truly offline

Have we actually built an offline-first to-do app?  Almost, but not quite.  While we can now store all data offline, if you switch off your device’s Internet connection and try loading the application, it won’t open.  To fix this, we need to use the **HTML5 Application Cache**.

## Warning

While HTML5 Application Cache works reasonably well for a simple single-page application like this, it doesn’t always. Thoroughly research how it works before considering whether to apply it to your website.

- Service Worker might soon replace HTML5 Application Cache, although it is not currently usable in any browser, and neither Apple nor Microsoft have publicly committed to supporting it.
- To enable the application cache, we’ll add a `manifest` attribute to the `html` element of the web page.

We will cover the essential hacks needed to get AppCache to behave itself later in the course.

##### `/index.html`

```html
<!DOCTYPE html>
<html manifest="./offline.appcache">
[…]
```

Then, we’ll create a manifest file, which is a simple text file in which we crudely specify the files to make available offline and how we want the cache to behave.

##### `/offline.appcache`

```
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js

NETWORK:
*
```

The section that begins `CACHE MANIFEST` tells the browser the following:

- When the application is first accessed, download each of those files and store them in the application cache.
- Any time any of those files are needed from then on, load the cached versions of the files, rather than redownload them from the Internet.

The section that begins `NETWORK` tells the browser that all other files must be downloaded fresh from the Internet every time they are needed.

(Yes, the default behaviour is to say any file not listed in the AppCache should never be downloaded - even if the device is online!?)

## Check

Verify the application works offline by loading it up and then switching off the static web server (or if you're using a webserver disconnect your device from the internet) and press refresh.

---

[← Back to *review: `window.indexedDB`*](../11-review-requests-transactions) | [Continue to *first appcache gotcha* →](../12-appcache-gotcha-1)


================================================
FILE: 03-offline-todo/11-appcache/application.js
================================================
(function() {
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.querySelector('input');
			ul = document.querySelector('ul');
			document.body.addEventListener('submit', onSubmit);
			document.body.addEventListener('click', onClick);
		})
		.then(refreshView);

	function onClick(e) {
		e.preventDefault();
		if (e.target.hasAttribute('id')) {
			databaseTodosGetById(e.target.getAttribute('id'))
				.then(function(todo) {
					return databaseTodosDelete(todo);
				})
				.then(refreshView);
		}
	}

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet().then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet() {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					data.push(result.value);
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseTodosGetById(id) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.get(id);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseTodosDelete(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.delete(todo._id);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 03-offline-todo/11-appcache/index.html
================================================
<!DOCTYPE html>
<html manifest="./offline.appcache">
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 03-offline-todo/11-appcache/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js

NETWORK:
*


================================================
FILE: 03-offline-todo/11-appcache/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 03-offline-todo/11-appcache/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 03-offline-todo/12-appcache-gotcha-1/README.md
================================================
# AppCache Gotchas

Load up your app, edit a file (e.g. change the background colour in the CSS file).  Refresh the app.  What happens?

Your web application is now **offline first**.  That means it will load from cache first, do a background update and your users will *only see that update* when they press refresh.

Press refresh.  What do you see now?

With AppCache, a background update will only happen *if the application manifest itself changes*.  Because you haven't updated the `offline.appcache` file it won't update.

Now try loading the web application in an Private Browsing Window (Incognito in Chrome).  Notice it gets the most up-to-date version of the web application.

Now add a comment into the application cache, like this:

##### `offline.appcache`

```
CACHE MANIFEST
# v2
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js

NETWORK:
*
```

And with the developer console open, refresh both the non-private browsing and the private browsing application.

Notice that both update and re-download all the assets required for the application to load

## First AppCache Gotchas:-

- Updates happen in the background.  To see updates you must refresh the application **twice**.
- For AppCache enabled web applications to update, the application cache manifest file must change.
- When an AppCache update happens *all files* get re-downloaded.  Even if you only make a one line change in a single CSS file.

#### Exercises

- What happens if you set `Cache-Control` headers for the application cache for a very long time, for example a year or more?

#### Solutions

- That user gets stuck on that version of your application *for a year or more*.

---

[← Back to *appcache*](../11-appcache) | [Continue to *success* →](../13-success)


================================================
FILE: 03-offline-todo/13-success/README.md
================================================
# Success!

We’ve created a quick and simple to-do app that works offline and that runs in all major modern browsers, thanks to both IndexedDB and WebSQL (via a polyfill).

## Exercises

- What user journey and technical implemention might to make the offline experience optional?  Implementing opt-in is _relatively easy_.  **Implement a mechanism to uninstall the application.**
  - _Hint for AppCache: [If the manifest itself returns a 404 or 410, the cache is deleted](http://www.html5rocks.com/en/tutorials/appcache/beginner/)._


- Currently, whenever a todo is *created* or *deleted* the application throws away everything on screen, loads all todos from the database and puts them in the DOM.
  - Make this more efficient.

## Bonus exercises

- We've cheated a bit (for reasons that will become clearer in the next section) and used the created timestamp as the key for todos.  IndexedDB also supports auto-incrementors.
  - Create a new version of the database that uses an incrementor for the `_id` and write a migration within `onupgradeneeded` that changes the structure of the database to use an auto-incrementor and updates the data to use the new index.
  - What happens if you have two tabs open and one tab upgrades to the new schema and javascript whilst the other is still expecting the old schema?

---

[← Back to *appcache gotchas*](../12-appcache-gotcha-1) | [Continue to *AppCacheOffline Todo with IndexedDB and sync* →](../../04-offline-todo-with-sync)


================================================
FILE: 03-offline-todo/README.md
================================================
# Offline todo with IndexedDB

We’re going to make a simple offline-first [to-do application](https://matthew-andrews.github.io/offline-todo/) with HTML5
technology.  Here is what the app will do:

- store data offline and load without an Internet connection;
- allow the user to add and delete items in the to-do list;
- store all data locally, with no back end;</li>
- run on the first- and second-most recent versions of all major desktop and mobile browsers.

## Which technologies to use

In an ideal world, we’d use just one client database technology. Unfortunately, we’ll have to use two:

- **IndexedDB** This is the standard for client-side storage and the [only option available on Firefox and Internet
Explorer](http://caniuse.com/indexeddb).
- **WebSQL** This is the deprecated predecessor to WebSQL and the [only option available on current versions of
iOS](http://caniuse.com/sql-storage) (although iOS 8 will finally give us IndexedDB).
</ul>

Veterans of the offline-first world might now be thinking, “But we could just use
[localStorage](http://caniuse.com/namevalue-storage), which has the benefits of a much simpler API, and we wouldn’t
need to worry about the complexity of using both IndexedDB and WebSQL.” While that is technically true,
[localStorage has number of problems](https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/),
the most important of which is that the amount of storage space available with it is significantly less than IndexedDB
and WebSQL.

Luckily, while we’ll need to use both, we’ll only need to think about IndexedDB. To support WebSQL, we’ll use an
[IndexedDB polyfill](https://github.com/axemclion/IndexedDBShim). This will keep our code clean and easy to maintain,
and once all browsers that we care about support IndexedDB natively, we can simply delete the polyfill.

**Note:** If you’re starting a new project and are deciding whether to use IndexedDB or WebSQL, I strongly advocate
using IndexedDB and the polyfill. In my opinion, there is no reason to write any new code that integrates with WebSQL
directly.

I’ll go through all of the steps using Google Chrome (and its developer tools), but there’s no reason why you couldn’t
develop this application using any other modern browser.

---

[← Back to *Fetch*](../02-new-apis/fetch.md) | [Start by *scaffolding the application* →](./01-scaffolding)


================================================
FILE: 04-offline-todo-with-sync/01-architecture/README.md
================================================
# Architecture

As with the simple offline todo app, we're going to take lots of shortcuts so that we can focus on covering the key ideas.

## Synchronisation

There are many ways to store data so that it can be synchronized and merged in with other changes that have happened since the last synchronisation.  For example, **git** stores changes made 'offline' as sequences of **diffs**.

For our todo application we are going to take the simplest possible route use the following algorithm for synchronising with the server:

- download all todos from the server, load all todos from the local database
- loop through all local todos
  - if a todo has been deleted locally, delete it from the server
  - if a todo isn't in the array of todos returned from the server, assume it's new and create it on the server
- loop through all the remote todos
  - if a todo isn't in the array of todos loaded from the local database, assume it's been created by another client since the last synchronisation and create it locally.

This algorithm is clearly extremely inefficient as it requires downloading all the todos in one go, it requires holding all the todos that exist in memory at once, and it requires walking through all the data to figure out what needs updating.  We get away with this because the size of the data we expect for our todos is quite small.

### Delete Corollary 1: we can't just delete todos anymore

By choosing this approach to synchronisation if we continue to directly delete todos from the local database as we are at the moment, the synchronisation algorithm isn't going to be able to distinguish todos that have been previously synced with the server but deleted locallly from todos that have been added by other clients but haven't yet been downloaded.

To work around this instead of deleting todos we will *mark todos for deletion* and only delete them once we are sure that the server has successfully deleted them.

### Delete Corollary 2: the server can never delete todos

Again because of the choice to use this approach to synchronisation in order for clients to be able to distinguish between todos that have been just created locally and not synced (and so exist locally but not on the server) and those that have been deleted by other clients (and so exist locally but not on the server) the server can never *actually* delete todos - only mark them for deletion.

Luckily the API we've chosen to use already [does this already](https://github.com/matthew-andrews/offline-todo-api#delete-todosid---delete-a-todo) and returns a `410 Gone` response for all `POST` and `GET` requests keyed for todos with `_id`s that have already been deleted.

## Appropriate `_id`s

With distributed applications where it is possible to create records on clients independently from the server choosing a sensible way to uniquely identify records turns out to be challenging.

### Why you can't just use auto-increment to create a local ID and let the server fill in a remote ID later:

- Say you create a new todo and it was allocated an ID of 2 whilst your browser is offline.
- You have the application open in two tabs when the browser connects to the internet and both begin syncing at the same time.
- Both simulataneously `POST` the new todo to the server.
- Because the server hasn't yet allocated the todo its permanent unique identifier, it cannot distinguish the two todos from each other and so creates it twice.

You could work around this by requiring all todo text to be unique but that would probably be undesireable for a todo app.

#### Other alternatives

- **Hash the todo and use it as an ID.**  Also requires todo text to be unique with the additional problem that once you've created and deleted a todo item you can never re-add a todo item with the same text again.
- **Generate a UUID.** The FT solved a similar problem to solve in their article authoring tool by generating a 36 character unique identifier for all articles with a one-in-a-million-like chance of colliding with another.  This would work well but adds complexity to the application.
- **Only support the creation of todos online.** Worthy of a mention and useful for existing systems.  Afterall, *some* offline behaviour is a huge improvement on *none*.
- Use the **timestamp** at the point the todo was created.  Although there are likely to be bugs collisions if two clients create a todo at precisely the same moment.  Also relies on the device's clock being set to some extent.

### Conclusion

There is no simple solution that works in all cases.  For real world applications (that use this synchronisation algorithm) I would either choose to use some sort of uuid or to try to find a way to make local ids with remote ids lazily filled tab-safe.

For the purpose of this demonstration prototype we will we use the **timestamp** option because it's by far the simplest.

---

[← Back to *offline todo with IndexedDB and sync*](../) | [Continue to *mark for deletion* →](../02-mark-for-deletion)


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/README.md
================================================
# Marking items for deletion

Rather than directly deleting todo items we need to change our client side code to *mark todos for deletion*.

To achieve this we are going to need to make the following changes to `application.js`:

- change the `onClick` handler, which deletes individual todos, from directly deleting the todo to setting a new `deleted` property to `true`
- enhance the `databaseTodosGet` method so that todos can be filtered by their deleted status.
- update `refreshView`'s use of `databaseTodosGet` so that it only renders undeleted todos.
- we won't delete the `databaseTodosDelete` method, even though it is now unused as the synchronisation logic that we will implement later will make use of it.

##### `application.js`

```js
[…]

  function onClick(e) {
    e.preventDefault();
    if (e.target.hasAttribute('id')) {
      databaseTodosGetById(e.target.getAttribute('id'))
        .then(function(todo) {
          todo.deleted = true;
          return databaseTodosPut(todo);
        })
        .then(refreshView);
    }
  }

[…]

  function refreshView() {
    return databaseTodosGet({ deleted: false }).then(renderAllTodos);
  }

[…]

  function databaseTodosGet(query) {
    return new Promise(function(resolve, reject) {
      var transaction = db.transaction(['todo'], 'readonly');
      var store = transaction.objectStore('todo');

      var keyRange = IDBKeyRange.lowerBound(0);
      var cursorRequest = store.openCursor(keyRange);

      var data = [];
      cursorRequest.onsuccess = function(e) {
        var result = e.target.result;

        if (result) {
          if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
            data.push(result.value);
          }
          result.continue();

        } else {
          resolve(data);
        }
      };
    });
  }

[…]
```

Assuming nothing has broken the application should continue working in exactly the same way it did before - you should be able to create and delete todos.  The difference is an implementation detail that can be checked by opening up dev tools - where you will see that todos don't actually get deleted any more - they only have a `deleted` flag set to `true` against them.  See below:

!['Hallo Welt' has been flagged for deletion](./screenshot.png)

---

[← Back to *architecture*](../01-architecture) | [Continue to *adding ajax* →](../03-adding-ajax)


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/application.js
================================================
(function() {
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.getElementsByTagName('input')[0];
			ul = document.getElementsByTagName('ul')[0];
			document.body.addEventListener('submit', onSubmit);
			document.body.addEventListener('click', onClick);
		})
		.then(refreshView);

	function onClick(e) {
		e.preventDefault();
		if (e.target.hasAttribute('id')) {
			databaseTodosGetById(e.target.getAttribute('id'))
				.then(function(todo) {
					todo.deleted = true;
					return databaseTodosPut(todo);
				})
				.then(refreshView);
		}
	}

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet({ deleted: false }).then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet(query) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
						data.push(result.value);
					}
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseTodosGetById(id) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');
			var request = store.get(id);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseTodosDelete(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.delete(todo._id);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

}());


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/index.html
================================================
<!DOCTYPE html>
<html manifest="./offline.appcache">
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js

NETWORK:
*


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/README.md
================================================
# Adding ajax

In order to communicate with the server we're going to need to implement some ajax functionality to communicate with our **[offline-todo-api](https://github.com/matthew-andrews/offline-todo-api)**.  I'm going to suggest using Fetch [(well, a polyfill of it)](https://github.com/github/fetch) but you could use `XMLHttpRequest`, jQuery or other libraries if you prefer.

## Install ajax library

As the Fetch API isn't implemented in any browsers yet, we will need to use a polyfill instead, which will mean we will need to make changes to `index.html`, `offline.appcache` and create a new file `fetch.js`.

##### `index.html`

```html
<!DOCTYPE html>
<html manifest="./offline.appcache">
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./fetch.js"></script>
    <script src="./application.js"></script>
  </body>
</html>
```

##### `offline.appcache`

```
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js

NETWORK:
*
```

##### `fetch.js`

Download the contents of the [Fetch API polyfill](https://raw.githubusercontent.com/github/fetch/master/fetch.js), and put it in this file.

## Api wrapper methods

Now we have an ajax library installed and available to us in our application we are next going to implement a few helper methods that mirror the API of the database methods:

##### `application.js`

```js
(function() {
  var api = 'https://offline-todo-api.herokuapp.com/todos';

[…]

  function serverTodosGet(_id) {
    return fetch(api + '/' + (_id ? _id : ''))
      .then(function(response) {
        return response.json();
      });
  }

  function serverTodosPost(todo) {
    return fetch(api, {
        method: 'post',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(todo)
      })
        .then(function(response) {
          if (response.status === 410) throw new Error(response.statusText);
	  return response;
	});
  }

  function serverTodosDelete(todo) {
    return fetch(api + '/' + todo._id, { method: 'delete' })
  }
}());
```

---

[← Back to *mark for deletion*](../02-mark-for-deletion) | [Continue to *synchronize* →](../04-synchronize)


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/application.js
================================================
(function() {
	var api = 'https://offline-todo-api.herokuapp.com/todos';
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.getElementsByTagName('input')[0];
			ul = document.getElementsByTagName('ul')[0];
			document.body.addEventListener('submit', onSubmit);
			document.body.addEventListener('click', onClick);
		})
		.then(refreshView);

	function onClick(e) {
		e.preventDefault();
		if (e.target.hasAttribute('id')) {
			databaseTodosGetById(e.target.getAttribute('id'))
				.then(function(todo) {
					todo.deleted = true;
					return databaseTodosPut(todo);
				})
				.then(refreshView);
		}
	}

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet({ deleted: false }).then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet(query) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
						data.push(result.value);
					}
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseTodosGetById(id) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');
			var request = store.get(id);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseTodosDelete(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.delete(todo._id);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function serverTodosGet(_id) {
		return fetch(api + '/' + (_id ? _id : ''))
			.then(function(response) {
				return response.json();
			});
	}

	function serverTodosPost(todo) {
		return fetch(api, {
				method: 'post',
				headers: {
					'Accept': 'application/json',
					'Content-Type': 'application/json'
				},
				body: JSON.stringify(todo)
			})
				.then(function(response) {
					if (response.status === 410) throw new Error(response.statusText);
		return response;
	});
	}

	function serverTodosDelete(todo) {
		return fetch(api + '/' + todo._id, { method: 'delete' });
	}
}());


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/fetch.js
================================================
(function() {
  'use strict';

  if (window.fetch) {
    return
  }

  function Headers(headers) {
    this.map = {}

    var self = this
    if (headers instanceof Headers) {
      headers.forEach(function(name, values) {
        values.forEach(function(value) {
          self.append(name, value)
        })
      })

    } else if (headers) {
      Object.getOwnPropertyNames(headers).forEach(function(name) {
        self.append(name, headers[name])
      })
    }
  }

  Headers.prototype.append = function(name, value) {
    var list = this.map[name]
    if (!list) {
      list = []
      this.map[name] = list
    }
    list.push(value)
  }

  Headers.prototype['delete'] = function(name) {
    delete this.map[name]
  }

  Headers.prototype.get = function(name) {
    var values = this.map[name]
    return values ? values[0] : null
  }

  Headers.prototype.getAll = function(name) {
    return this.map[name] || []
  }

  Headers.prototype.has = function(name) {
    return this.map.hasOwnProperty(name)
  }

  Headers.prototype.set = function(name, value) {
    this.map[name] = [value]
  }

  // Instead of iterable for now.
  Headers.prototype.forEach = function(callback) {
    var self = this
    Object.getOwnPropertyNames(this.map).forEach(function(name) {
      callback(name, self.map[name])
    })
  }

  function consumed(body) {
    if (body.bodyUsed) {
      return Promise.reject(new TypeError('Body already consumed'))
    }
    body.bodyUsed = true
  }

  function Body() {
    this.body = null
    this.bodyUsed = false

    this.arrayBuffer = function() {
      throw new Error('Not implemented yet')
    }

    this.blob = function() {
      var rejected = consumed(this)
      return rejected ? rejected : Promise.resolve(new Blob([this.body]))
    }

    this.formData = function() {
      return Promise.resolve(decode(this.body))
    }

    this.json = function() {
      var rejected = consumed(this)
      if (rejected) {
        return rejected
      }

      var body = this.body
      return new Promise(function(resolve, reject) {
        try {
          resolve(JSON.parse(body))
        } catch (ex) {
          reject(ex)
        }
      })
    }

    this.text = function() {
      var rejected = consumed(this)
      return rejected ? rejected : Promise.resolve(this.body)
    }

    return this
  }

  function Request(url, options) {
    options = options || {}
    this.url = url
    this.body = options.body
    this.credentials = options.credentials || null
    this.headers = new Headers(options.headers)
    this.method = options.method || 'GET'
    this.mode = options.mode || null
    this.referrer = null
  }

  function encode(params) {
    return Object.getOwnPropertyNames(params).filter(function(name) {
      return params[name] !== undefined
    }).map(function(name) {
      var value = (params[name] === null) ? '' : params[name]
      return encodeURIComponent(name) + '=' + encodeURIComponent(value)
    }).join('&').replace(/%20/g, '+')
  }

  function decode(body) {
    var form = new FormData()
    body.trim().split('&').forEach(function(bytes) {
      if (bytes) {
        var split = bytes.split('=')
        var name = split.shift().replace(/\+/g, ' ')
        var value = split.join('=').replace(/\+/g, ' ')
        form.append(decodeURIComponent(name), decodeURIComponent(value))
      }
    })
    return form
  }

  function isObject(value) {
    try {
      return Object.getPrototypeOf(value) === Object.prototype
    } catch (ex) {
      // Probably a string literal.
      return false
    }
  }

  function headers(xhr) {
    var head = new Headers()
    var pairs = xhr.getAllResponseHeaders().trim().split('\n')
    pairs.forEach(function(header) {
      var split = header.trim().split(':')
      var key = split.shift().trim()
      var value = split.join(':').trim()
      head.append(key, value)
    })
    return head
  }

  Request.prototype.fetch = function() {
    var self = this

    return new Promise(function(resolve, reject) {
      var xhr = new XMLHttpRequest()

      xhr.onload = function() {
        var options = {
          status: xhr.status,
          statusText: xhr.statusText,
          headers: headers(xhr)
        }
        resolve(new Response(xhr.responseText, options))
      }

      xhr.onerror = function() {
        reject()
      }

      xhr.open(self.method, self.url)

      self.headers.forEach(function(name, values) {
        values.forEach(function(value) {
          xhr.setRequestHeader(name, value)
        })
      })

      var body = self.body
      if (isObject(self.body)) {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
        body = encode(self.body)
      }
      xhr.send(body)
    })
  }

  Body.call(Request.prototype)

  function Response(body, options) {
    this.body = body
    this.type = 'default'
    this.url = null
    this.status = options.status
    this.statusText = options.statusText
    this.headers = options.headers
  }

  Body.call(Response.prototype)

  window.fetch = function (url, options) {
    return new Request(url, options).fetch()
  }
})();

================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/index.html
================================================
<!DOCTYPE html>
<html manifest="./offline.appcache">
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./fetch.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js

NETWORK:
*


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/README.md
================================================
# Synchronize

As a reminder the algorithm we decided to use for synchronization was:

- download all todos from the server, load all todos from the local database
- loop through all local todos
  - if a todo has been deleted locally, delete it from the server
  - if a todo isn't in the array of todos returned from the server, assume it's new and create it on the server
- loop through all the remote todos
  - if a todo isn't in the array of todos loaded from the local database, assume it's been created by another client since the last synchronisation and create it locally.

##### `application.js`

```js
[…]

  function synchronize() {
    return Promise.all([serverTodosGet(), databaseTodosGet()])
      .then(function(results) {
        var promises = [];
        var remoteTodos = results[0];
        var localTodos = results[1];

        // Loop through local todos and if they haven't been
        // posted to the server, post them.
        promises = promises.concat(localTodos.map(function(todo) {
          var deleteTodo = function() {
            return databaseTodosDelete(todo);
          };

          // Has it been marked for deletion?
          if (todo.deleted) {
            return serverTodosDelete(todo).then(deleteTodo, function(res) {
              if (err.message === "Gone") return deleteTodo();
            });
          }

          // If this is a todo that doesn't exist on the server try to create
          // it (if it fails because it's gone, delete it locally)
          if (!arrayContainsTodo(remoteTodos, todo)) {
            return serverTodosPost(todo)
              .catch(function(err) {
                if (err.message === "Gone") return deleteTodo(todo);
              });
          }
        }));

        // Go through the todos that came down from the server,
        // we don't already have one, add it to the local db
        promises = promises.concat(remoteTodos.map(function(todo) {
          if (!arrayContainsTodo(localTodos, todo)) {
            return databaseTodosPut(todo);
          }
        }));
        return Promise.all(promises);
    }, function(err) {
      console.error(err, "Cannot connect to server");
    })
    .then(refreshView);
  }

  function arrayContainsTodo(array, todo) {
    for (var i = 0; i < array.length; i++) {
       if(array[i]._id === todo._id) {
         return true;
       }
    };
    return false;
  }

[…]
```


---

[← Back to *adding data*](../03-adding-ajax) | [Continue to *success* →](../05-success)


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/application.js
================================================
(function() {
	var api = 'https://offline-todo-api.herokuapp.com/todos';
	var db, input, ul;

	databaseOpen()
		.then(function() {
			input = document.getElementsByTagName('input')[0];
			ul = document.getElementsByTagName('ul')[0];
			document.body.addEventListener('submit', onSubmit);
			document.body.addEventListener('click', onClick);
		})
		.then(refreshView);

	function onClick(e) {
		e.preventDefault();
		if (e.target.hasAttribute('id')) {
			databaseTodosGetById(e.target.getAttribute('id'))
				.then(function(todo) {
					todo.deleted = true;
					return databaseTodosPut(todo);
				})
				.then(refreshView);
		}
	}

	function onSubmit(e) {
		e.preventDefault();
		var todo = { text: input.value, _id: String(Date.now()) };
		databaseTodosPut(todo)
			.then(function() {
				input.value = '';
			})
			.then(refreshView);
	}

	function refreshView() {
		return databaseTodosGet({ deleted: false }).then(renderAllTodos);
	}

	function renderAllTodos(todos) {
		var html = '';
		todos.forEach(function(todo) {
			html += todoToHtml(todo);
		});
		ul.innerHTML = html;
	}

	function todoToHtml(todo) {
		return '<li><button id="'+todo._id+'">delete</button>'+todo.text+'</li>';
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('todos', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('todo', { keyPath: '_id' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseTodosPut(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.put(todo);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function databaseTodosGet(query) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');

			// Get everything in the store
			var keyRange = IDBKeyRange.lowerBound(0);
			var cursorRequest = store.openCursor(keyRange);

			// This fires once per row in the store, so for simplicity collect the data
			// in an array (data) and send it pass it in the resolve call in one go
			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;

				// If there's data, add it to array
				if (result) {
					if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
						data.push(result.value);
					}
					result.continue();

				// Reach the end of the data
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseTodosGetById(id) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readonly');
			var store = transaction.objectStore('todo');
			var request = store.get(id);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseTodosDelete(todo) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['todo'], 'readwrite');
			var store = transaction.objectStore('todo');
			var request = store.delete(todo._id);
			transaction.oncomplete = resolve;
			request.onerror = reject;
		});
	}

	function serverTodosGet(_id) {
		return fetch(api + '/' + (_id ? _id : ''))
			.then(function(response) {
				return response.json();
			});
	}

	function serverTodosPost(todo) {
		return fetch(api, {
				method: 'post',
				headers: {
					'Accept': 'application/json',
					'Content-Type': 'application/json'
				},
				body: JSON.stringify(todo)
			})
				.then(function(response) {
					if (response.status === 410) throw new Error(response.statusText);
		return response;
	});
	}

	function serverTodosDelete(todo) {
		return fetch(api + '/' + todo._id, { method: 'delete' });
	}
}());


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/fetch.js
================================================
(function() {
  'use strict';

  if (window.fetch) {
    return
  }

  function Headers(headers) {
    this.map = {}

    var self = this
    if (headers instanceof Headers) {
      headers.forEach(function(name, values) {
        values.forEach(function(value) {
          self.append(name, value)
        })
      })

    } else if (headers) {
      Object.getOwnPropertyNames(headers).forEach(function(name) {
        self.append(name, headers[name])
      })
    }
  }

  Headers.prototype.append = function(name, value) {
    var list = this.map[name]
    if (!list) {
      list = []
      this.map[name] = list
    }
    list.push(value)
  }

  Headers.prototype['delete'] = function(name) {
    delete this.map[name]
  }

  Headers.prototype.get = function(name) {
    var values = this.map[name]
    return values ? values[0] : null
  }

  Headers.prototype.getAll = function(name) {
    return this.map[name] || []
  }

  Headers.prototype.has = function(name) {
    return this.map.hasOwnProperty(name)
  }

  Headers.prototype.set = function(name, value) {
    this.map[name] = [value]
  }

  // Instead of iterable for now.
  Headers.prototype.forEach = function(callback) {
    var self = this
    Object.getOwnPropertyNames(this.map).forEach(function(name) {
      callback(name, self.map[name])
    })
  }

  function consumed(body) {
    if (body.bodyUsed) {
      return Promise.reject(new TypeError('Body already consumed'))
    }
    body.bodyUsed = true
  }

  function Body() {
    this.body = null
    this.bodyUsed = false

    this.arrayBuffer = function() {
      throw new Error('Not implemented yet')
    }

    this.blob = function() {
      var rejected = consumed(this)
      return rejected ? rejected : Promise.resolve(new Blob([this.body]))
    }

    this.formData = function() {
      return Promise.resolve(decode(this.body))
    }

    this.json = function() {
      var rejected = consumed(this)
      if (rejected) {
        return rejected
      }

      var body = this.body
      return new Promise(function(resolve, reject) {
        try {
          resolve(JSON.parse(body))
        } catch (ex) {
          reject(ex)
        }
      })
    }

    this.text = function() {
      var rejected = consumed(this)
      return rejected ? rejected : Promise.resolve(this.body)
    }

    return this
  }

  function Request(url, options) {
    options = options || {}
    this.url = url
    this.body = options.body
    this.credentials = options.credentials || null
    this.headers = new Headers(options.headers)
    this.method = options.method || 'GET'
    this.mode = options.mode || null
    this.referrer = null
  }

  function encode(params) {
    return Object.getOwnPropertyNames(params).filter(function(name) {
      return params[name] !== undefined
    }).map(function(name) {
      var value = (params[name] === null) ? '' : params[name]
      return encodeURIComponent(name) + '=' + encodeURIComponent(value)
    }).join('&').replace(/%20/g, '+')
  }

  function decode(body) {
    var form = new FormData()
    body.trim().split('&').forEach(function(bytes) {
      if (bytes) {
        var split = bytes.split('=')
        var name = split.shift().replace(/\+/g, ' ')
        var value = split.join('=').replace(/\+/g, ' ')
        form.append(decodeURIComponent(name), decodeURIComponent(value))
      }
    })
    return form
  }

  function isObject(value) {
    try {
      return Object.getPrototypeOf(value) === Object.prototype
    } catch (ex) {
      // Probably a string literal.
      return false
    }
  }

  function headers(xhr) {
    var head = new Headers()
    var pairs = xhr.getAllResponseHeaders().trim().split('\n')
    pairs.forEach(function(header) {
      var split = header.trim().split(':')
      var key = split.shift().trim()
      var value = split.join(':').trim()
      head.append(key, value)
    })
    return head
  }

  Request.prototype.fetch = function() {
    var self = this

    return new Promise(function(resolve, reject) {
      var xhr = new XMLHttpRequest()

      xhr.onload = function() {
        var options = {
          status: xhr.status,
          statusText: xhr.statusText,
          headers: headers(xhr)
        }
        resolve(new Response(xhr.responseText, options))
      }

      xhr.onerror = function() {
        reject()
      }

      xhr.open(self.method, self.url)

      self.headers.forEach(function(name, values) {
        values.forEach(function(value) {
          xhr.setRequestHeader(name, value)
        })
      })

      var body = self.body
      if (isObject(self.body)) {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
        body = encode(self.body)
      }
      xhr.send(body)
    })
  }

  Body.call(Request.prototype)

  function Response(body, options) {
    this.body = body
    this.type = 'default'
    this.url = null
    this.status = options.status
    this.statusText = options.statusText
    this.headers = options.headers
  }

  Body.call(Response.prototype)

  window.fetch = function (url, options) {
    return new Request(url, options).fetch()
  }
})();

================================================
FILE: 04-offline-todo-with-sync/04-synchronize/index.html
================================================
<!DOCTYPE html>
<html manifest="./offline.appcache">
  <head>
    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />
  </head>
  <body>
    <h1>Example: Todo</h1>
    <form>
      <input placeholder="Type something" />
    </form>
    <ul>
    </ul>
    <script src="./indexeddb.shim.min.js"></script>
    <script src="./promise.js"></script>
    <script src="./fetch.js"></script>
    <script src="./application.js"></script>
  </body>
</html>


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js

NETWORK:
*


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j<a.length;j++)g=a[j],g&&e(g.then)?g.then(d(j),c):f(j,g)})}var d=a.isArray,e=a.isFunction;b.all=c}),a("promise/asap",["exports"],function(a){"use strict";function b(){return function(){process.nextTick(e)}}function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.observe(c,{characterData:!0}),function(){c.data=a=++a%2}}function d(){return function(){j.setTimeout(e,1)}}function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k=[]}function f(a,b){var c=k.push([a,b]);1===c&&g()}var g,h="undefined"!=typeof window?window:{},i=h.MutationObserver||h.WebKitMutationObserver,j="undefined"!=typeof global?global:void 0===this?window:this,k=[];g="undefined"!=typeof process&&"[object process]"==={}.toString.call(process)?b():i?c():d(),a.asap=f}),a("promise/config",["exports"],function(a){"use strict";function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}var c={instrument:!1};a.config=c,a.configure=b}),a("promise/polyfill",["./promise","./utils","exports"],function(a,b,c){"use strict";function d(){var a;a="undefined"!=typeof global?global:"undefined"!=typeof window&&window.document?window:self;var b="Promise"in a&&"resolve"in a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;return new a.Promise(function(a){b=a}),f(b)}();b||(a.Promise=e)}var e=a.Promise,f=b.isFunction;c.polyfill=d}),a("promise/promise",["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],function(a,b,c,d,e,f,g,h){"use strict";function i(a){if(!v(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof i))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");this._subscribers=[],j(a,this)}function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}catch(e){d(e)}}function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!0,f=j}else e=d,g=!0;n(b,e)||(i&&g?o(b,e):h?q(b,f):a===D?o(b,e):a===E&&q(b,e))}function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+E]=d}function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;g+=3)c=e[g],d=e[g+b],k(b,c,d,f);a._subscribers=null}function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promises callback cannot return that same promise.");if(u(b)&&(d=b.then,v(d)))return d.call(b,function(d){return c?!0:(c=!0,b!==d?o(a,d):p(a,d),void 0)},function(b){return c?!0:(c=!0,q(a,b),void 0)}),!0}catch(e){return c?!0:(q(a,e),!0)}return!1}function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}function r(a){m(a,a._state=D)}function s(a){m(a,a._state=E)}var t=a.config,u=(a.configure,b.objectOrFunction),v=b.isFunction,w=(b.now,c.all),x=d.race,y=e.resolve,z=f.reject,A=g.asap;t.async=A;var B=void 0,C=0,D=1,E=2;i.prototype={constructor:i,_state:void 0,_detail:void 0,_subscribers:void 0,then:function(a,b){var c=this,d=new this.constructor(function(){});if(this._state){var e=arguments;t.async(function(){k(c._state,d,e[c._state-1],c._detail)})}else l(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}},i.all=w,i.race=x,i.resolve=y,i.reject=z,h.Promise=i}),a("promise/race",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to race.");return new b(function(b,c){for(var d,e=0;e<a.length;e++)d=a[e],d&&"function"==typeof d.then?d.then(b,c):b(d)})}var d=a.isArray;b.race=c}),a("promise/reject",["exports"],function(a){"use strict";function b(a){var b=this;return new b(function(b,c){c(a)})}a.reject=b}),a("promise/resolve",["exports"],function(a){"use strict";function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;var b=this;return new b(function(b){b(a)})}a.resolve=b}),a("promise/utils",["exports"],function(a){"use strict";function b(a){return c(a)||"object"==typeof a&&null!==a}function c(a){return"function"==typeof a}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}var e=Date.now||function(){return(new Date).getTime()};a.objectOrFunction=b,a.isFunction=c,a.isArray=d,a.now=e}),b("promise/polyfill").polyfill()}();


================================================
FILE: 04-offline-todo-with-sync/04-synchronize/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}

* {
  box-sizing: border-box;
}

h1 {
  padding: 18px 20px;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}

form {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

input {
  width: 100%;
  padding: 6px;
  font-size: 1.4em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  padding: 20px;
  border-bottom: solid 1px #DDD;
}

button {
  float: right;
}


================================================
FILE: 04-offline-todo-with-sync/05-success/README.md
================================================
# Success!

## Bonus exercises

- Currently the synchronization logic can run multiple times simulataneously.  Although we have to handle this for the case of multiple tabs open in the same browser so *should work* it is quite wasteful and will generate a lot of unnecessary http requests.  How could you change the synchronization logic so that it will not start if it's already running?
- The **[offline-todo-api](https://github.com/matthew-andrews/offline-todo-api)** offers a [server sent events](http://www.html5rocks.com/en/tutorials/eventsource/basics/) end point that will stream notifications to clients whenever a todo is created or deleted.  Add code to subscribe to this stream so that todos that are deleted or created by other devices synchronize near instantly.  What other technologies would work here?
- We've sort of hacked filtering by `deleted` status into `databaseTodosGet` and are currently quite wasteful - they will retrieve *all* the todos before filtering.  Write a migration that adds `deleted` as an index and enhance `databaseTodosGet` so that we can read just the todos we are interested in directly from IndexedDB.

---

[← Back to *synchronize*](../04-synchronize) | [Continue to *building an offline news website* →](../../05-offline-news)


================================================
FILE: 04-offline-todo-with-sync/README.md
================================================
# Offline todo with IndexedDB and synchronization

We will now enhance our simple offline-first to-do application by hooking it up to a server so that, when it has an internet connection, it can synchronize its data.  Here is what the app will do:

- everything that the previous app did, on as many browsers;
- synchronizes its todos with the server, deleting todos on the server that have been deleted locally, added todos on the server that have been created locally.
- works across different browsers on different devices, different browsers on the same device, browsers in private browsing mode and across different open tabs.

---

[← Back to *success*](../03-offline-todo/10-success) | [Continue to *architecture* →](01-architecture)


================================================
FILE: 05-offline-news/01-scaffolding/README.md
================================================
# Scaffolding

This assumes familiarity with [*npm*](https://www.npmjs.org/), `package.json` and [*express*](http://expressjs.com/).

## Set up a quick *Express* server

In a new folder, create some files and a directory:-

- [`/public`](./public) - a new directory
- [`/public/styles.css`](./public/style.css) - some simple styles
- [`/public/templates.js`](./public/templates.js) - templating functions that will be shared between the server and the client
- [`/index.js`](./index.js) - this will be our express server
- [`/package.json`](./package.json) - this will be for listing our dependencies, initially all this will need to contain is `{}`.


```
echo '{}' >> test.json # Neat trick if you're using *nix
npm install --save express cookie-parser es6-promise isomorphic-fetch
```

## Online only

To begin with we'll make the application work online only (ie. a normal website) and then discuss the changes that we will have to make to make it work offline.  As building normal websites is an assumed prerequisite of this course heavy use of copy-paste is encouragedhere unless anything is unclear:

##### [`public/styles.css`](./public/styles.css)

```css
body {
	margin: 0;
	padding: 0;
	font-family: helvetica, sans-serif;
}
* {
	box-sizing: border-box;
}
h1 {
	padding: 14px 0 14px 0;
	margin: 0;
	font-size: 44px;
	border-bottom: solid 1px #DDD;
	line-height: 1em;
}
nav {
	padding: 14px 0 14px 0;
}
main {
	padding: 0 14px;
}
ul {
	padding: 0;
	margin: 0;
	list-style: none;
}
li {
	padding: 20px 0 20px 0;
	border-bottom: solid 1px #DDD;
}
```

Nothing too surprising should jump out here - it's just plain CSS.

##### [`public/templates.js`](./public/templates.js)

```js
(function() {
	var exports = {
		list: list,
		article: article
	};

	function list(data) {
		data = data || [];
		var ul = '';
		data.forEach(function(story) {
			ul += '<li><a class="js-link" href="/article/'+story.guid+'">'+story.title+'</a></li>';
		});
		return '<h1>FT Tech Blog</h1><ul>'+ul+'</ul>';
	}

	function article(data) {
		return '<nav><a class="js-link" href="/">&raquo; Back to FT Tech Blog</a></nav><h1>'+data.title+'</h1>'+data.body;
	}

	if (typeof module == 'object') {
		module.exports = exports;
	} else {
		window.templates = exports;
	}
}());
```

As we discussed above, these functions will eventually be used on the client side - which is why they need to go inside `public`.

`list` and `article` are functions that take a JavaScript object containing data that represent a list of stories and a single story, respectively.

The last few lines are potentially a little confusing:-

```js
if (typeof module == 'object') {
	module.exports = exports;
} else {
	window.templates = exports;
}
```

`if (typeof module == 'object')` is just a way of saying "am I running on the server" - and if that is the case this module will expose its functions via `module.exports`, otherwise it will add them to the `window` object.

##### [`index.js`](./index.js)

```js
require('es6-promise').polyfill();
require('isomorphic-fetch');

var port = Number(process.env.PORT || 8080);
var api = 'https://offline-news-api.herokuapp.com/stories';

var cookieParser = require('cookie-parser');
var express = require('express');
var path = require('path');
var templates = require('./public/templates');

var app = express();
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// Manifest returns a 400 unless the AppCache cookie is set
app.get('/offline.appcache', function(req, res) {
	if (req.cookies.up) {
		res.set('Content-Type', 'text/cache-manifest');
		res.send('CACHE MANIFEST'
			+ '\n./appcache.js'
			+ '\n./application.js'
			+ '\n./iframe.js'
			+ '\n./indexeddb.shim.min.js'
			+ '\n./promise.js'
			+ '\n./styles.css'
			+ '\n./fetch.js'
			+ '\n./templates.js'
			+ '\n'
			+ '\nFALLBACK:'
			+ '\n/ /'
			+ '\n'
			+ '\nNETWORK:'
			+ '\n*');
	} else {
		res.status(400).end();
	}
});

// Add middleware to send this when the appcache update cookie is set
app.get('/', offlineMiddleware);
app.get('/article/:guid', offlineMiddleware);

function offlineMiddleware(req, res, next) {
	if (req.cookies.up) res.send(layoutShell());
	else next();
}

app.get('/fallback.html', function(req, res) {
	res.send(layoutShell());
});

app.get('/article/:guid', function(req, res) {
	fetch(api+'/'+req.params.guid)
		.then(function(response) {
			return response.json();
		})
		.then(function(data) {
			res.send(layoutShell({
				main: templates.article(data)
			}));
		}, function(err) {
			res.status(404);
			res.send(layoutShell({
				main: templates.article({
					title: 'Story cannot be found',
					body: '<p>Please try another</p>'
				})
			}));
		});
});

app.get('/', function(req, res) {
	fetch(api)
		.then(function(response) {
			return response.json();
		})
		.then(function(data) {
			res.send(layoutShell({
				main: templates.list(data)
			}));
		}, function(err) {
			res.status(404).end();
		});
});

function layoutShell(data) {
	data = {
		title: data && data.title || 'FT Tech News',
		main: data && data.main || ''
	};
	return '<!DOCTYPE html>'
		+ '\n<html>'
		+ '\n  <head>'
		+ '\n    <title>'+data.title+'</title>'
		+ '\n    <link rel="stylesheet" href="/styles.css" type="text/css" media="all" />'
		+ '\n  </head>'
		+ '\n  <body>'
		+ '\n    <div class="brandrews"><a href="https://mattandre.ws">mattandre.ws</a> | <a href="https://twitter.com/andrewsmatt">@andrewsmatt</a></div>'
		+ '\n    <main>'+data.main+'</main>'
		+ '\n    <script src="/indexeddb.shim.min.js"></script>'
		+ '\n    <script src="/fetch.js"></script>'
		+ '\n    <script src="/promise.js"></script>'
		+ '\n    <script src="/templates.js"></script>'
		+ '\n    <script src="/appcache.js"></script>'
		+ '\n    <script>'
		+ '\n      (function(i,s,o,g,r,a,m){i[\'GoogleAnalyticsObject\']=r;i[r]=i[r]||function(){'
		+ '\n      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),'
		+ '\n      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)'
		+ '\n      })(window,document,\'script\',\'//www.google-analytics.com/analytics.js\',\'ga\');'
		+ '\n      ga(\'create\', \'UA-34192510-7\', \'auto\');'
		+ '\n      ga(\'send\', \'pageview\');'
		+ '\n    </script>'
		+ '\n    <script src="/application.js"></script>'
		+ '\n  </body>'
		+ '\n</html>';
}

app.listen(port);
console.log('listening on '+port);
```

Now run the application in your favourite web browser and check that both views work by running:-

```js
node index.js
```

And opening `http://localhost:8080` with your favourite browser.

---

[← Back to *building and offline news app, FT style*](../) | [Continue to *single/multi-page app* →](../02-single-multi-page)


================================================
FILE: 05-offline-news/01-scaffolding/index.js
================================================
require('es6-promise').polyfill();
require('isomorphic-fetch');

var api = 'https://offline-news-api.herokuapp.com/stories';
var port = 8080;
var express = require('express');
var path = require('path');
var templates = require('./public/templates');

var app = express();
app.use(express.static(path.join(__dirname, 'public')));

app.get('/article/:guid', function(req, res) {
	fetch(api+'/'+req.params.guid)
		.then(function(response) {
			return response.json();
		})
		.then(function(data) {
			res.send(layoutShell({
				main: templates.article(data)
			}));
		}, function(err) {
			res.status(404);
			res.send(layoutShell({
				main: templates.article({
					title: 'Story cannot be found',
					body: '<p>Please try another</p>'
				})
			}));
		});
});

app.get('/', function(req, res) {
	fetch(api)
		.then(function(response) {
			return response.json();
		})
		.then(function(data) {
			res.send(layoutShell({
				main: templates.list(data)
			}));
		}, function(err) {
			res.status(404).end();
		});
});

function layoutShell(data) {
	data = {
		title: data && data.title || 'FT Tech News',
		main: data && data.main || ''
	};
	return '<!DOCTYPE html>'
		+ '\n<html>'
		+ '\n  <head>'
		+ '\n    <title>'+data.title+'</title>'
		+ '\n    <link rel="stylesheet" href="/styles.css" type="text/css" media="all" />'
		+ '\n  </head>'
		+ '\n  <body>'
		+ '\n    <main>'+data.main+'</main>'
		+ '\n  </body>'
		+ '\n</html>';
}

app.listen(port);
console.log('listening on port', port);


================================================
FILE: 05-offline-news/01-scaffolding/package.json
================================================
{
  "dependencies": {
    "cookie-parser": "^1.3.3",
    "es6-promise": "^2.0.0",
    "express": "^4.8.8",
    "isomorphic-fetch": "^1.0.0"
  }
}


================================================
FILE: 05-offline-news/01-scaffolding/public/styles.css
================================================
body {
  margin: 0;
  padding: 0;
  font-family: helvetica, sans-serif;
}
* {
  box-sizing: border-box;
}
h1 {
  padding: 14px 0 14px 0;
  margin: 0;
  font-size: 44px;
  border-bottom: solid 1px #DDD;
  line-height: 1em;
}
nav {
  padding: 14px 0 14px 0;
}
main {
  padding: 0 14px;
}
ul {
  padding: 0;
  margin: 0;
  list-style: none;
}
li {
  padding: 20px 0 20px 0;
  border-bottom: solid 1px #DDD;
}


================================================
FILE: 05-offline-news/01-scaffolding/public/templates.js
================================================
(function() {
	var exports = {
		list: list,
		article: article
	};

	function list(data) {
		data = data || [];
		var ul = '';
		data.forEach(function(story) {
			ul += '<li><a class="js-link" href="/article/'+story.guid+'">'+story.title+'</a></li>';
		});
		return '<h1>FT Tech Blog</h1><ul>'+ul+'</ul>';
	}

	function article(data) {
		return '<nav><a class="js-link" href="/">&raquo; Back to FT Tech Blog</a></nav><h1>'+data.title+'</h1>'+data.body;
	}

	if (typeof module == 'object') {
		module.exports = exports;
	} else {
		window.templates = exports;
	}
}());


================================================
FILE: 05-offline-news/02-single-multi-page/README.md
================================================
# Single/Multi page app

Back in the introduction we laid down some basic ground rules about which offline technologies we were going to use to store the different kinds of data we need to.

- We will use **Application Cache** to store application assets, such as CSS, JavaScript and a basic HTML shell.
- We will use **IndexedDB** to store content - in this case, articles.

Because content stored in IndexedDB is not accessible to the Application Cache, in order to display pages offline we the website will need to transform into a single page app - whilst still being a normal website (multi-page app?) for the initial load.

In this step we will implement synchronisation and client-side and server-side rendering.

##### [`/index.js`](./index.js)

On the server side we will need to add a few more JavaScript files to the `layoutShell` function.

```js
[…]

		+ '\n  </head>'
		+ '\n  <body>'
		+ '\n    <main>'+data.main+'</main>'
		+ '\n    <script src="/indexeddb.shim.min.js"></script>'
		+ '\n    <script src="/fetch.js"></script>'
		+ '\n    <script src="/promise.js"></script>'
		+ '\n    <script src="/templates.js"></script>'
		+ '\n    <script src="/application.js"></script>'
		+ '\n  </body>'
		+ '\n</html>';

[…]
```

##### Add libraries and polyfills

Copy over the polyfills for [**IndexedDB**](./public/indexeddb.shim.min.js), [**Promises**](./public/promise.js) and the [**Fetch API polyfill**](./public/fetch.js) library from our previous prototypes and place the JavaScript files in `public`.

##### [`/application.js`](./public/application.js)

1. Opens a database
2. Synchronises with the `/stories`, adding stories that have been added and deleting stories that have been removed
3. Uses `window.templates.list()` and `window.templates.article()` to render those articles.
4. Uses `history.pushState` to update the URL when a user clicks on an article or when they click to view list of articles and refreshes the content on screen with that of the requested page.
5. Bonus: integrate this with Google analytics so that link click that is handled on the client side is turned into a `pageview` event and tracked.

Try to do use the work we have done already in previous prototypes copying the solution.

```js
(function() {
	var api = 'https://offline-news-api.herokuapp.com/stories';
	var synchronizeInProgress;
	var db, main;

	databaseOpen()
		.then(function() {
			main = document.querySelector('main');
			document.body.addEventListener('click', onClick);
			window.addEventListener('popstate', refreshView);

			// Only refresh the view if the view is empty
			if (main.innerHTML === '') return refreshView();
		})
		.then(synchronize);

	function onClick(e) {
		if (e.target.classList.contains('js-link')) {
			e.preventDefault();
			history.pushState({}, '', e.target.getAttribute('href'));
			refreshView();
		}
	}

	function refreshView() {
		var guidMatches = location.pathname.match(/^\/article\/([0-9]+)/);
		if (!guidMatches) {
			renderAllStories();
			return databaseStoriesGet().then(renderAllStories);
		}
		renderOneStory();
		return databaseStoriesGetById(guidMatches[1]).then(renderOneStory);
	}

	function renderAllStories(stories) {
		main.innerHTML = templates.list(stories);
	}

	function renderOneStory(story) {
		if (!story) story = { title: 'Story cannot be found', body: '<p>Please try another</p>' };
		main.innerHTML = templates.article(story);
	}

	function synchronize() {
		if (synchronizeInProgress) return synchronizeInProgress;
		synchronizeInProgress = Promise.all([serverStoriesGet(), databaseStoriesGet()])
			.then(function(results) {
				var promises = [];
				var remoteStories = results[0];
				var localStories = results[1];

				// Add new stories downloaded from server to the database
				promises = promises.concat(remoteStories.map(function(story) {
					if (!arrayContainsStory(localStories, story)) {
						return databaseStoriesPut(story);
					}
				}));

				// Delete stories that are no longer on the server from the database
				promises = promises.concat(localStories.map(function(story) {
					if (!arrayContainsStory(remoteStories, story)) {
						return databaseStoriesDelete(story);
					}
				}));

				return promises;
			})

			// Only refresh the view if it's listing page
			.then(function(results) {
				if (location.pathname === '/') {
					return refreshView();
				}
			})
			.then(function() {
				synchronizeInProgress = undefined;
			});
	}

	function arrayContainsStory(array, story) {
		return array.some(function(arrayStory) {
			return arrayStory.guid === story.guid;
		});
	}

	function databaseOpen() {
		return new Promise(function(resolve, reject) {
			var version = 1;
			var request = indexedDB.open('news-server-rendered', version);
			request.onupgradeneeded = function(e) {
				db = e.target.result;
				e.target.transaction.onerror = reject;
				db.createObjectStore('stories', { keyPath: 'guid' });
			};
			request.onsuccess = function(e) {
				db = e.target.result;
				resolve();
			};
			request.onerror = reject;
		});
	}

	function databaseStoriesPut(story) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['stories'], 'readwrite');
			var store = transaction.objectStore('stories');
			var request = store.put(story);
			request.onsuccess = resolve;
			request.onerror = reject;
		});
	}

	function databaseStoriesGet() {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['stories'], 'readonly');
			var store = transaction.objectStore('stories');

			var keyRange = IDBKeyRange.lowerBound(0);

			// Using reverse direction because the index being sorted on
			// ends with a numerical incrementing ID so to get newest news
			// first you need to sort by largest first.
			var cursorRequest = store.openCursor(keyRange, 'prev');

			var data = [];
			cursorRequest.onsuccess = function(e) {
				var result = e.target.result;
				if (result) {
					data.push(result.value);
					result.continue();
				} else {
					resolve(data);
				}
			};
		});
	}

	function databaseStoriesGetById(guid) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['stories'], 'readonly');
			var store = transaction.objectStore('stories');
			var request = store.get(guid);
			request.onsuccess = function(e) {
				var result = e.target.result;
				resolve(result);
			};
			request.onerror = reject;
		});
	}

	function databaseStoriesDelete(story) {
		return new Promise(function(resolve, reject) {
			var transaction = db.transaction(['stories'], 'readwrite');
			var store = transaction.objectStore('stories');
			var request = store.delete(story.guid);
			request.onsuccess = resolve;
			request.onerror = reject;
		});
	}

	function serverStoriesGet(guid) {
		return fetch(api + '/' + (guid ? guid : ''))
			.then(function(response) {
				return response.json();
			});
	}
})();
```

---

[← Back to *scaffolding*](../01-scaffolding) | [Continue to *hacking appcache* →](../03-hacking-appcache)


================================================
FILE: 05-offline-news/02-single-multi-page/index.js
================================================
require('es6-promise').polyfill();
require('isomorphic-fetch');

var api = 'https://offline-news-api.herokuapp.com/stories';
var port = 8080;
var express = require('express');
var path = require('path');
var templates = require('./public/templates');

var app = express();
app.use(express.static(path.join(__dirname, 'public')));

app.get('/article/:guid', function(req, res) {
	fetch(api+'/'+req.params.guid)
		.then(function(response) {
			return response.json();
		})
		.then(function(data) {
			res.send(layoutShell({
				main: templates.article(data)
			}));
		}, function(err) {
			res.status
Download .txt
gitextract_2g5j4x9v/

├── .gitignore
├── .jshintrc
├── 01-introduction/
│   ├── README.md
│   ├── dysfunctional-family.md
│   ├── how.md
│   └── why.md
├── 02-new-apis/
│   ├── README.md
│   ├── fetch.md
│   └── solutions.md
├── 03-offline-todo/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 02-opening-a-database/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 03-using-dev-tools/
│   │   └── README.md
│   ├── 04-creating-object-stores/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 05-review-window-indexeddb/
│   │   └── README.md
│   ├── 06-adding-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 07-getting-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 08-rendering-todos/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 09-deleting-data/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 10-review-requests-transactions/
│   │   └── README.md
│   ├── 11-appcache/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 12-appcache-gotcha-1/
│   │   └── README.md
│   ├── 13-success/
│   │   └── README.md
│   └── README.md
├── 04-offline-todo-with-sync/
│   ├── 01-architecture/
│   │   └── README.md
│   ├── 02-mark-for-deletion/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 03-adding-ajax/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── fetch.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 04-synchronize/
│   │   ├── README.md
│   │   ├── application.js
│   │   ├── fetch.js
│   │   ├── index.html
│   │   ├── offline.appcache
│   │   ├── promise.js
│   │   └── styles.css
│   ├── 05-success/
│   │   └── README.md
│   └── README.md
├── 05-offline-news/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 02-single-multi-page/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 03-hacking-appcache/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── iframe.html
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 04-more-hacking-appcache/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── appcache.js
│   │       ├── application.js
│   │       ├── fetch.js
│   │       ├── iframe.html
│   │       ├── iframe.js
│   │       ├── promise.js
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 05-success/
│   │   └── README.md
│   └── README.md
├── 06-offline-news-with-service-worker/
│   ├── 01-scaffolding/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   └── public/
│   │       ├── styles.css
│   │       └── templates.js
│   ├── 02-registering-a-service-worker/
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   ├── application.js
│   │   │   ├── service-worker.js
│   │   │   ├── styles.css
│   │   │   └── templates.js
│   │   ├── styles.css
│   │   └── templates.js
│   ├── 03-service-worker-caches/
│   │   └── README.md
│   ├── 04-success/
│   │   └── README.md
│   └── README.md
├── 07-dexie/
│   └── README.md
├── 08-success/
│   └── README.md
├── README.md
└── package.json
Download .txt
SYMBOL INDEX (545 symbols across 45 files)

FILE: 03-offline-todo/01-scaffolding/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/02-opening-a-database/application.js
  function databaseOpen (line 11) | function databaseOpen() {

FILE: 03-offline-todo/02-opening-a-database/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/04-creating-object-stores/application.js
  function databaseOpen (line 11) | function databaseOpen() {

FILE: 03-offline-todo/04-creating-object-stores/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/06-adding-data/application.js
  function onSubmit (line 10) | function onSubmit(e) {
  function databaseOpen (line 19) | function databaseOpen() {
  function databaseTodosPut (line 36) | function databaseTodosPut(todo) {

FILE: 03-offline-todo/06-adding-data/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/07-getting-data/application.js
  function onSubmit (line 14) | function onSubmit(e) {
  function databaseOpen (line 23) | function databaseOpen() {
  function databaseTodosPut (line 40) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 50) | function databaseTodosGet() {

FILE: 03-offline-todo/07-getting-data/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/08-rendering-todos/application.js
  function onSubmit (line 12) | function onSubmit(e) {
  function refreshView (line 22) | function refreshView() {
  function renderAllTodos (line 26) | function renderAllTodos(todos) {
  function todoToHtml (line 34) | function todoToHtml(todo) {
  function databaseOpen (line 38) | function databaseOpen() {
  function databaseTodosPut (line 55) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 65) | function databaseTodosGet() {

FILE: 03-offline-todo/08-rendering-todos/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/09-deleting-data/application.js
  function onClick (line 13) | function onClick(e) {
  function onSubmit (line 24) | function onSubmit(e) {
  function refreshView (line 34) | function refreshView() {
  function renderAllTodos (line 38) | function renderAllTodos(todos) {
  function todoToHtml (line 46) | function todoToHtml(todo) {
  function databaseOpen (line 50) | function databaseOpen() {
  function databaseTodosPut (line 67) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 77) | function databaseTodosGet() {
  function databaseTodosGetById (line 105) | function databaseTodosGetById(id) {
  function databaseTodosDelete (line 118) | function databaseTodosDelete(todo) {

FILE: 03-offline-todo/09-deleting-data/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 03-offline-todo/11-appcache/application.js
  function onClick (line 13) | function onClick(e) {
  function onSubmit (line 24) | function onSubmit(e) {
  function refreshView (line 34) | function refreshView() {
  function renderAllTodos (line 38) | function renderAllTodos(todos) {
  function todoToHtml (line 46) | function todoToHtml(todo) {
  function databaseOpen (line 50) | function databaseOpen() {
  function databaseTodosPut (line 67) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 77) | function databaseTodosGet() {
  function databaseTodosGetById (line 105) | function databaseTodosGetById(id) {
  function databaseTodosDelete (line 118) | function databaseTodosDelete(todo) {

FILE: 03-offline-todo/11-appcache/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 04-offline-todo-with-sync/02-mark-for-deletion/application.js
  function onClick (line 13) | function onClick(e) {
  function onSubmit (line 25) | function onSubmit(e) {
  function refreshView (line 35) | function refreshView() {
  function renderAllTodos (line 39) | function renderAllTodos(todos) {
  function todoToHtml (line 47) | function todoToHtml(todo) {
  function databaseOpen (line 51) | function databaseOpen() {
  function databaseTodosPut (line 68) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 78) | function databaseTodosGet(query) {
  function databaseTodosGetById (line 108) | function databaseTodosGetById(id) {
  function databaseTodosDelete (line 121) | function databaseTodosDelete(todo) {

FILE: 04-offline-todo-with-sync/02-mark-for-deletion/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 04-offline-todo-with-sync/03-adding-ajax/application.js
  function onClick (line 14) | function onClick(e) {
  function onSubmit (line 26) | function onSubmit(e) {
  function refreshView (line 36) | function refreshView() {
  function renderAllTodos (line 40) | function renderAllTodos(todos) {
  function todoToHtml (line 48) | function todoToHtml(todo) {
  function databaseOpen (line 52) | function databaseOpen() {
  function databaseTodosPut (line 69) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 79) | function databaseTodosGet(query) {
  function databaseTodosGetById (line 109) | function databaseTodosGetById(id) {
  function databaseTodosDelete (line 122) | function databaseTodosDelete(todo) {
  function serverTodosGet (line 132) | function serverTodosGet(_id) {
  function serverTodosPost (line 139) | function serverTodosPost(todo) {
  function serverTodosDelete (line 154) | function serverTodosDelete(todo) {

FILE: 04-offline-todo-with-sync/03-adding-ajax/fetch.js
  function Headers (line 8) | function Headers(headers) {
  function consumed (line 64) | function consumed(body) {
  function Body (line 71) | function Body() {
  function Request (line 112) | function Request(url, options) {
  function encode (line 123) | function encode(params) {
  function decode (line 132) | function decode(body) {
  function isObject (line 145) | function isObject(value) {
  function headers (line 154) | function headers(xhr) {
  function Response (line 204) | function Response(body, options) {

FILE: 04-offline-todo-with-sync/03-adding-ajax/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 04-offline-todo-with-sync/04-synchronize/application.js
  function onClick (line 14) | function onClick(e) {
  function onSubmit (line 26) | function onSubmit(e) {
  function refreshView (line 36) | function refreshView() {
  function renderAllTodos (line 40) | function renderAllTodos(todos) {
  function todoToHtml (line 48) | function todoToHtml(todo) {
  function databaseOpen (line 52) | function databaseOpen() {
  function databaseTodosPut (line 69) | function databaseTodosPut(todo) {
  function databaseTodosGet (line 79) | function databaseTodosGet(query) {
  function databaseTodosGetById (line 109) | function databaseTodosGetById(id) {
  function databaseTodosDelete (line 122) | function databaseTodosDelete(todo) {
  function serverTodosGet (line 132) | function serverTodosGet(_id) {
  function serverTodosPost (line 139) | function serverTodosPost(todo) {
  function serverTodosDelete (line 154) | function serverTodosDelete(todo) {

FILE: 04-offline-todo-with-sync/04-synchronize/fetch.js
  function Headers (line 8) | function Headers(headers) {
  function consumed (line 64) | function consumed(body) {
  function Body (line 71) | function Body() {
  function Request (line 112) | function Request(url, options) {
  function encode (line 123) | function encode(params) {
  function decode (line 132) | function decode(body) {
  function isObject (line 145) | function isObject(value) {
  function headers (line 154) | function headers(xhr) {
  function Response (line 204) | function Response(body, options) {

FILE: 04-offline-todo-with-sync/04-synchronize/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 05-offline-news/01-scaffolding/index.js
  function layoutShell (line 47) | function layoutShell(data) {

FILE: 05-offline-news/01-scaffolding/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 16) | function article(data) {

FILE: 05-offline-news/02-single-multi-page/index.js
  function layoutShell (line 47) | function layoutShell(data) {

FILE: 05-offline-news/02-single-multi-page/public/application.js
  function onClick (line 17) | function onClick(e) {
  function refreshView (line 25) | function refreshView() {
  function renderAllStories (line 35) | function renderAllStories(stories) {
  function renderOneStory (line 39) | function renderOneStory(story) {
  function synchronize (line 44) | function synchronize() {
  function arrayContainsStory (line 80) | function arrayContainsStory(array, story) {
  function databaseOpen (line 86) | function databaseOpen() {
  function databaseStoriesPut (line 103) | function databaseStoriesPut(story) {
  function databaseStoriesGet (line 113) | function databaseStoriesGet() {
  function databaseStoriesGetById (line 138) | function databaseStoriesGetById(guid) {
  function databaseStoriesDelete (line 151) | function databaseStoriesDelete(story) {
  function serverStoriesGet (line 161) | function serverStoriesGet(guid) {

FILE: 05-offline-news/02-single-multi-page/public/fetch.js
  function Headers (line 8) | function Headers(headers) {
  function consumed (line 64) | function consumed(body) {
  function Body (line 71) | function Body() {
  function Request (line 112) | function Request(url, options) {
  function encode (line 123) | function encode(params) {
  function decode (line 132) | function decode(body) {
  function isObject (line 145) | function isObject(value) {
  function headers (line 154) | function headers(xhr) {
  function Response (line 204) | function Response(body, options) {

FILE: 05-offline-news/02-single-multi-page/public/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 05-offline-news/02-single-multi-page/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 16) | function article(data) {

FILE: 05-offline-news/03-hacking-appcache/index.js
  function layoutShell (line 64) | function layoutShell(data) {

FILE: 05-offline-news/03-hacking-appcache/public/application.js
  function onClick (line 17) | function onClick(e) {
  function refreshView (line 25) | function refreshView() {
  function renderAllStories (line 35) | function renderAllStories(stories) {
  function renderOneStory (line 39) | function renderOneStory(story) {
  function synchronize (line 44) | function synchronize() {
  function arrayContainsStory (line 80) | function arrayContainsStory(array, story) {
  function databaseOpen (line 86) | function databaseOpen() {
  function databaseStoriesPut (line 103) | function databaseStoriesPut(story) {
  function databaseStoriesGet (line 113) | function databaseStoriesGet() {
  function databaseStoriesGetById (line 138) | function databaseStoriesGetById(guid) {
  function databaseStoriesDelete (line 151) | function databaseStoriesDelete(story) {
  function serverStoriesGet (line 161) | function serverStoriesGet(guid) {

FILE: 05-offline-news/03-hacking-appcache/public/fetch.js
  function Headers (line 8) | function Headers(headers) {
  function consumed (line 64) | function consumed(body) {
  function Body (line 71) | function Body() {
  function Request (line 112) | function Request(url, options) {
  function encode (line 123) | function encode(params) {
  function decode (line 132) | function decode(body) {
  function isObject (line 145) | function isObject(value) {
  function headers (line 154) | function headers(xhr) {
  function Response (line 204) | function Response(body, options) {

FILE: 05-offline-news/03-hacking-appcache/public/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 05-offline-news/03-hacking-appcache/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 16) | function article(data) {

FILE: 05-offline-news/04-more-hacking-appcache/index.js
  function offlineMiddleware (line 43) | function offlineMiddleware(req, res, next) {
  function layoutShell (line 82) | function layoutShell(data) {

FILE: 05-offline-news/04-more-hacking-appcache/public/appcache.js
  function onMessage (line 16) | function onMessage(event) {
  function load (line 22) | function load() {
  function onEvent (line 37) | function onEvent(eventCode) {

FILE: 05-offline-news/04-more-hacking-appcache/public/application.js
  function onClick (line 17) | function onClick(e) {
  function refreshView (line 25) | function refreshView() {
  function renderAllStories (line 35) | function renderAllStories(stories) {
  function renderOneStory (line 39) | function renderOneStory(story) {
  function synchronize (line 44) | function synchronize() {
  function arrayContainsStory (line 80) | function arrayContainsStory(array, story) {
  function databaseOpen (line 86) | function databaseOpen() {
  function databaseStoriesPut (line 103) | function databaseStoriesPut(story) {
  function databaseStoriesGet (line 113) | function databaseStoriesGet() {
  function databaseStoriesGetById (line 138) | function databaseStoriesGetById(guid) {
  function databaseStoriesDelete (line 151) | function databaseStoriesDelete(story) {
  function serverStoriesGet (line 161) | function serverStoriesGet(guid) {

FILE: 05-offline-news/04-more-hacking-appcache/public/fetch.js
  function Headers (line 8) | function Headers(headers) {
  function consumed (line 64) | function consumed(body) {
  function Body (line 71) | function Body() {
  function Request (line 112) | function Request(url, options) {
  function encode (line 123) | function encode(params) {
  function decode (line 132) | function decode(body) {
  function isObject (line 145) | function isObject(value) {
  function headers (line 154) | function headers(xhr) {
  function Response (line 204) | function Response(body, options) {

FILE: 05-offline-news/04-more-hacking-appcache/public/iframe.js
  function checkNow (line 6) | function checkNow() {
  function checkIn (line 21) | function checkIn(ms) {
  function trigger (line 25) | function trigger(evt, hasChecked) {

FILE: 05-offline-news/04-more-hacking-appcache/public/promise.js
  function c (line 1) | function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.s...
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(){return function(){process.nextTick(e)}}
  function c (line 1) | function c(){var a=0,b=new i(e),c=document.createTextNode("");return b.o...
  function d (line 1) | function d(){return function(){j.setTimeout(e,1)}}
  function e (line 1) | function e(){for(var a=0;a<k.length;a++){var b=k[a],c=b[0],d=b[1];c(d)}k...
  function f (line 1) | function f(a,b){var c=k.push([a,b]);1===c&&g()}
  function b (line 1) | function b(a,b){return 2!==arguments.length?c[a]:(c[a]=b,void 0)}
  function d (line 1) | function d(){var a;a="undefined"!=typeof global?global:"undefined"!=type...
  function i (line 1) | function i(a){if(!v(a))throw new TypeError("You must pass a resolver fun...
  function j (line 1) | function j(a,b){function c(a){o(b,a)}function d(a){q(b,a)}try{a(c,d)}cat...
  function k (line 1) | function k(a,b,c,d){var e,f,g,h,i=v(c);if(i)try{e=c(d),g=!0}catch(j){h=!...
  function l (line 1) | function l(a,b,c,d){var e=a._subscribers,f=e.length;e[f]=b,e[f+D]=c,e[f+...
  function m (line 1) | function m(a,b){for(var c,d,e=a._subscribers,f=a._detail,g=0;g<e.length;...
  function n (line 1) | function n(a,b){var c,d=null;try{if(a===b)throw new TypeError("A promise...
  function o (line 1) | function o(a,b){a===b?p(a,b):n(a,b)||p(a,b)}
  function p (line 1) | function p(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(r,a))}
  function q (line 1) | function q(a,b){a._state===B&&(a._state=C,a._detail=b,t.async(s,a))}
  function r (line 1) | function r(a){m(a,a._state=D)}
  function s (line 1) | function s(a){m(a,a._state=E)}
  function c (line 1) | function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an ...
  function b (line 1) | function b(a){var b=this;return new b(function(b,c){c(a)})}
  function b (line 1) | function b(a){if(a&&"object"==typeof a&&a.constructor===this)return a;va...
  function b (line 1) | function b(a){return c(a)||"object"==typeof a&&null!==a}
  function c (line 1) | function c(a){return"function"==typeof a}
  function d (line 1) | function d(a){return"[object Array]"===Object.prototype.toString.call(a)}

FILE: 05-offline-news/04-more-hacking-appcache/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 16) | function article(data) {

FILE: 06-offline-news-with-service-worker/01-scaffolding/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 17) | function article(data) {
  function shell (line 24) | function shell(data) {

FILE: 06-offline-news-with-service-worker/02-registering-a-service-worker/public/templates.js
  function list (line 7) | function list(data) {
  function article (line 17) | function article(data) {
  function shell (line 24) | function shell(data) {

FILE: 06-offline-news-with-service-worker/02-registering-a-service-worker/templates.js
  function list (line 7) | function list(data) {
  function article (line 17) | function article(data) {
  function shell (line 24) | function shell(data) {
Condensed preview — 136 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (311K chars).
[
  {
    "path": ".gitignore",
    "chars": 15,
    "preview": "/node_modules/\n"
  },
  {
    "path": ".jshintrc",
    "chars": 56,
    "preview": "{\n\t\"browser\": true,\n\t\"laxbreak\": true,\n\t\"debug\": true\n}\n"
  },
  {
    "path": "01-introduction/README.md",
    "chars": 1798,
    "preview": "# Introduction\n\nToday we're going to explore all the technologies available in browsers today that can be brought togeth"
  },
  {
    "path": "01-introduction/dysfunctional-family.md",
    "chars": 3415,
    "preview": "# Meet the dysfunctional family\n\n## Quiz\n\n- Can you name all the offline storage technologies that exist in browsers tod"
  },
  {
    "path": "01-introduction/how.md",
    "chars": 1931,
    "preview": "# How\n\nThere are four distinct problems to solve*:\n\n1. Delivering the application\n2. Storing data\n3. Syncing data\n4. Thi"
  },
  {
    "path": "01-introduction/why.md",
    "chars": 1235,
    "preview": "# Why\n\nWe live in a bubble.  We work all day on high-spec machines, plugged into high-speed connections and use the late"
  },
  {
    "path": "02-new-apis/README.md",
    "chars": 6424,
    "preview": "# Promises\n\n## What is a Promise?\n\n> The Promise interface **represents a proxy for a value not necessarily known when t"
  },
  {
    "path": "02-new-apis/fetch.md",
    "chars": 2713,
    "preview": "# Fetch\n\nBy now I hope I've convinced you that ES6 Promises are the one and only solution to asynchronous flow and that "
  },
  {
    "path": "02-new-apis/solutions.md",
    "chars": 1631,
    "preview": "# Solutions\n\n```js\nfunction successful() {\n  return new Promise(resolve, reject) {\n    setTimeout(resolve, 1000);\n  };\n}"
  },
  {
    "path": "03-offline-todo/01-scaffolding/README.md",
    "chars": 2676,
    "preview": "# Scaffolding the application\n\nWe will create the following files in a single directory:\n\n- [`/index.html`](./index.html"
  },
  {
    "path": "03-offline-todo/01-scaffolding/application.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "03-offline-todo/01-scaffolding/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/01-scaffolding/offline.appcache",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "03-offline-todo/01-scaffolding/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/01-scaffolding/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/02-opening-a-database/README.md",
    "chars": 909,
    "preview": "# Opening an IndexedDB database\n\n##### `/application.js`\n\n```js\n(function() {\n  var db;\n\n  databaseOpen()\n    .then(func"
  },
  {
    "path": "03-offline-todo/02-opening-a-database/application.js",
    "chars": 616,
    "preview": "(function() {\n\n\t// 'global' variable to store reference to the database\n\tvar db;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t"
  },
  {
    "path": "03-offline-todo/02-opening-a-database/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/02-opening-a-database/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/02-opening-a-database/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/03-using-dev-tools/README.md",
    "chars": 1133,
    "preview": "# Using dev tools\n\n### Exercise\n\n- Find debug tools for viewing IndexedDB and WebSQL databases across all the major web "
  },
  {
    "path": "03-offline-todo/04-creating-object-stores/README.md",
    "chars": 3325,
    "preview": "# Creating object stores\n\nLike many database formats that you might be familiar with, you can create many tables in a si"
  },
  {
    "path": "03-offline-todo/04-creating-object-stores/application.js",
    "chars": 616,
    "preview": "(function() {\n\n\t// 'global' variable to store reference to the database\n\tvar db;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t"
  },
  {
    "path": "03-offline-todo/04-creating-object-stores/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/04-creating-object-stores/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/04-creating-object-stores/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/05-review-window-indexeddb/README.md",
    "chars": 2677,
    "preview": "## Review: `window.indexedDB`\n\nEverything related to **IndexedDB** is accessed either directly or indirectly through `wi"
  },
  {
    "path": "03-offline-todo/06-adding-data/README.md",
    "chars": 2597,
    "preview": "# Adding items\n\nThe next step is to enable the user to add items.\n\n##### `/application.js`\n\nNote that I’ve omitted the d"
  },
  {
    "path": "03-offline-todo/06-adding-data/application.js",
    "chars": 1111,
    "preview": "(function() {\n\tvar db, input;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.querySelector('input');\n\t\t\tdocum"
  },
  {
    "path": "03-offline-todo/06-adding-data/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/06-adding-data/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/06-adding-data/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/07-getting-data/README.md",
    "chars": 2107,
    "preview": "# Retrieving items\n\nNow that we’ve stored some data, the next step is to work out how to retrieve it.\n\n##### `/applicati"
  },
  {
    "path": "03-offline-todo/07-getting-data/application.js",
    "chars": 1961,
    "preview": "(function() {\n\tvar db, input;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.querySelector('input');\n\t\t\tdocum"
  },
  {
    "path": "03-offline-todo/07-getting-data/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/07-getting-data/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/07-getting-data/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/08-rendering-todos/README.md",
    "chars": 1616,
    "preview": "# Rendering todos\n\nIn this section we will put in the plumbing that will use the methods we've already implemented to re"
  },
  {
    "path": "03-offline-todo/08-rendering-todos/application.js",
    "chars": 2260,
    "preview": "(function() {\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.querySelector('input');\n\t\t\tu"
  },
  {
    "path": "03-offline-todo/08-rendering-todos/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/08-rendering-todos/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/08-rendering-todos/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/09-deleting-data/README.md",
    "chars": 3192,
    "preview": "# Deleting items\n\nTo keep things as simple as possible, we will let users delete items by clicking on a delete button ne"
  },
  {
    "path": "03-offline-todo/09-deleting-data/application.js",
    "chars": 3269,
    "preview": "(function() {\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.querySelector('input');\n\t\t\tu"
  },
  {
    "path": "03-offline-todo/09-deleting-data/index.html",
    "chars": 399,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/css' media='all' />\n  </head>\n"
  },
  {
    "path": "03-offline-todo/09-deleting-data/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/09-deleting-data/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/10-review-requests-transactions/README.md",
    "chars": 5521,
    "preview": "# Review `IDBRequest` and `IDBTransaction`\n\nNow that we've covered selecting, inserting and deleting data with IndexedDB"
  },
  {
    "path": "03-offline-todo/11-appcache/README.md",
    "chars": 2258,
    "preview": "# Truly offline\n\nHave we actually built an offline-first to-do app?  Almost, but not quite.  While we can now store all "
  },
  {
    "path": "03-offline-todo/11-appcache/application.js",
    "chars": 3269,
    "preview": "(function() {\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.querySelector('input');\n\t\t\tu"
  },
  {
    "path": "03-offline-todo/11-appcache/index.html",
    "chars": 429,
    "preview": "<!DOCTYPE html>\n<html manifest=\"./offline.appcache\">\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/"
  },
  {
    "path": "03-offline-todo/11-appcache/offline.appcache",
    "chars": 94,
    "preview": "CACHE MANIFEST\n./styles.css\n./indexeddb.shim.min.js\n./promise.js\n./application.js\n\nNETWORK:\n*\n"
  },
  {
    "path": "03-offline-todo/11-appcache/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "03-offline-todo/11-appcache/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "03-offline-todo/12-appcache-gotcha-1/README.md",
    "chars": 1769,
    "preview": "# AppCache Gotchas\n\nLoad up your app, edit a file (e.g. change the background colour in the CSS file).  Refresh the app."
  },
  {
    "path": "03-offline-todo/13-success/README.md",
    "chars": 1479,
    "preview": "# Success!\n\nWe’ve created a quick and simple to-do app that works offline and that runs in all major modern browsers, th"
  },
  {
    "path": "03-offline-todo/README.md",
    "chars": 2386,
    "preview": "# Offline todo with IndexedDB\n\nWe’re going to make a simple offline-first [to-do application](https://matthew-andrews.gi"
  },
  {
    "path": "04-offline-todo-with-sync/01-architecture/README.md",
    "chars": 5009,
    "preview": "# Architecture\n\nAs with the simple offline todo app, we're going to take lots of shortcuts so that we can focus on cover"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/README.md",
    "chars": 2442,
    "preview": "# Marking items for deletion\n\nRather than directly deleting todo items we need to change our client side code to *mark t"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/application.js",
    "chars": 3467,
    "preview": "(function() {\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(function() {\n\t\t\tinput = document.getElementsByTagName('input'"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/index.html",
    "chars": 429,
    "preview": "<!DOCTYPE html>\n<html manifest=\"./offline.appcache\">\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/offline.appcache",
    "chars": 94,
    "preview": "CACHE MANIFEST\n./styles.css\n./indexeddb.shim.min.js\n./promise.js\n./application.js\n\nNETWORK:\n*\n"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "04-offline-todo-with-sync/02-mark-for-deletion/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/README.md",
    "chars": 2501,
    "preview": "# Adding ajax\n\nIn order to communicate with the server we're going to need to implement some ajax functionality to commu"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/application.js",
    "chars": 4112,
    "preview": "(function() {\n\tvar api = 'https://offline-todo-api.herokuapp.com/todos';\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(fu"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/fetch.js",
    "chars": 5161,
    "preview": "(function() {\n  'use strict';\n\n  if (window.fetch) {\n    return\n  }\n\n  function Headers(headers) {\n    this.map = {}\n\n  "
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/index.html",
    "chars": 468,
    "preview": "<!DOCTYPE html>\n<html manifest=\"./offline.appcache\">\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/offline.appcache",
    "chars": 105,
    "preview": "CACHE MANIFEST\n./styles.css\n./indexeddb.shim.min.js\n./promise.js\n./fetch.js\n./application.js\n\nNETWORK:\n*\n"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "04-offline-todo-with-sync/03-adding-ajax/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/README.md",
    "chars": 2506,
    "preview": "# Synchronize\n\nAs a reminder the algorithm we decided to use for synchronization was:\n\n- download all todos from the ser"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/application.js",
    "chars": 4112,
    "preview": "(function() {\n\tvar api = 'https://offline-todo-api.herokuapp.com/todos';\n\tvar db, input, ul;\n\n\tdatabaseOpen()\n\t\t.then(fu"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/fetch.js",
    "chars": 5161,
    "preview": "(function() {\n  'use strict';\n\n  if (window.fetch) {\n    return\n  }\n\n  function Headers(headers) {\n    this.map = {}\n\n  "
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/index.html",
    "chars": 468,
    "preview": "<!DOCTYPE html>\n<html manifest=\"./offline.appcache\">\n  <head>\n    <link rel='stylesheet' href='./styles.css' type='text/"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/offline.appcache",
    "chars": 105,
    "preview": "CACHE MANIFEST\n./styles.css\n./indexeddb.shim.min.js\n./promise.js\n./fetch.js\n./application.js\n\nNETWORK:\n*\n"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "04-offline-todo-with-sync/04-synchronize/styles.css",
    "chars": 485,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nh1 {\n  paddi"
  },
  {
    "path": "04-offline-todo-with-sync/05-success/README.md",
    "chars": 1274,
    "preview": "# Success!\n\n## Bonus exercises\n\n- Currently the synchronization logic can run multiple times simulataneously.  Although "
  },
  {
    "path": "04-offline-todo-with-sync/README.md",
    "chars": 741,
    "preview": "# Offline todo with IndexedDB and synchronization\n\nWe will now enhance our simple offline-first to-do application by hoo"
  },
  {
    "path": "05-offline-news/01-scaffolding/README.md",
    "chars": 6726,
    "preview": "# Scaffolding\n\nThis assumes familiarity with [*npm*](https://www.npmjs.org/), `package.json` and [*express*](http://expr"
  },
  {
    "path": "05-offline-news/01-scaffolding/index.js",
    "chars": 1495,
    "preview": "require('es6-promise').polyfill();\nrequire('isomorphic-fetch');\n\nvar api = 'https://offline-news-api.herokuapp.com/stori"
  },
  {
    "path": "05-offline-news/01-scaffolding/package.json",
    "chars": 146,
    "preview": "{\n  \"dependencies\": {\n    \"cookie-parser\": \"^1.3.3\",\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.8.8\",\n    \"isomorph"
  },
  {
    "path": "05-offline-news/01-scaffolding/public/styles.css",
    "chars": 406,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n* {\n  box-sizing: border-box;\n}\nh1 {\n  padding"
  },
  {
    "path": "05-offline-news/01-scaffolding/public/templates.js",
    "chars": 569,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tdata = data || [];\n\t\tvar u"
  },
  {
    "path": "05-offline-news/02-single-multi-page/README.md",
    "chars": 7019,
    "preview": "# Single/Multi page app\n\nBack in the introduction we laid down some basic ground rules about which offline technologies "
  },
  {
    "path": "05-offline-news/02-single-multi-page/index.js",
    "chars": 1750,
    "preview": "require('es6-promise').polyfill();\nrequire('isomorphic-fetch');\n\nvar api = 'https://offline-news-api.herokuapp.com/stori"
  },
  {
    "path": "05-offline-news/02-single-multi-page/package.json",
    "chars": 146,
    "preview": "{\n  \"dependencies\": {\n    \"cookie-parser\": \"^1.3.3\",\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.8.8\",\n    \"isomorph"
  },
  {
    "path": "05-offline-news/02-single-multi-page/public/application.js",
    "chars": 4666,
    "preview": "(function() {\n\tvar api = 'https://offline-news-api.herokuapp.com/stories';\n\tvar synchronizeInProgress;\n\tvar db, main;\n\n\t"
  },
  {
    "path": "05-offline-news/02-single-multi-page/public/fetch.js",
    "chars": 5161,
    "preview": "(function() {\n  'use strict';\n\n  if (window.fetch) {\n    return\n  }\n\n  function Headers(headers) {\n    this.map = {}\n\n  "
  },
  {
    "path": "05-offline-news/02-single-multi-page/public/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "05-offline-news/02-single-multi-page/public/styles.css",
    "chars": 406,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n* {\n  box-sizing: border-box;\n}\nh1 {\n  padding"
  },
  {
    "path": "05-offline-news/02-single-multi-page/public/templates.js",
    "chars": 569,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tdata = data || [];\n\t\tvar u"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/README.md",
    "chars": 3059,
    "preview": "# Hacking AppCache\n\nTry to implement AppCache using the same approach we tried for our previous prototypes.  Use `chrome"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/index.js",
    "chars": 2229,
    "preview": "require('es6-promise').polyfill();\nrequire('isomorphic-fetch');\n\nvar api = 'https://offline-news-api.herokuapp.com/stori"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/package.json",
    "chars": 146,
    "preview": "{\n  \"dependencies\": {\n    \"cookie-parser\": \"^1.3.3\",\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.8.8\",\n    \"isomorph"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/application.js",
    "chars": 4666,
    "preview": "(function() {\n\tvar api = 'https://offline-news-api.herokuapp.com/stories';\n\tvar synchronizeInProgress;\n\tvar db, main;\n\n\t"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/fetch.js",
    "chars": 5161,
    "preview": "(function() {\n  'use strict';\n\n  if (window.fetch) {\n    return\n  }\n\n  function Headers(headers) {\n    this.map = {}\n\n  "
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/iframe.html",
    "chars": 130,
    "preview": "<!DOCTYPE html>\n<html manifest=\"/offline.appcache\">\n  <head>\n    <title>FT Tech News</title>\n  </head>\n  <body>\n  </body"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/styles.css",
    "chars": 406,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n* {\n  box-sizing: border-box;\n}\nh1 {\n  padding"
  },
  {
    "path": "05-offline-news/03-hacking-appcache/public/templates.js",
    "chars": 569,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tdata = data || [];\n\t\tvar u"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/README.md",
    "chars": 8474,
    "preview": "# More hacking AppCache\n\nWe've solved the AppCache problem for all pages except for the index page.\n\nThe home page of ou"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/index.js",
    "chars": 2596,
    "preview": "require('es6-promise').polyfill();\nrequire('isomorphic-fetch');\n\nvar api = 'https://offline-news-api.herokuapp.com/stori"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/package.json",
    "chars": 146,
    "preview": "{\n  \"dependencies\": {\n    \"cookie-parser\": \"^1.3.3\",\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.8.8\",\n    \"isomorph"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/appcache.js",
    "chars": 1625,
    "preview": "(function() {\n\tvar cookie = 'up';\n\tvar statuses = {\n\t\t\"-1\": 'timeout',\n\t\t\"0\": 'uncached',\n\t\t\"1\": 'idle',\n\t\t\"2\": 'checkin"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/application.js",
    "chars": 4666,
    "preview": "(function() {\n\tvar api = 'https://offline-news-api.herokuapp.com/stories';\n\tvar synchronizeInProgress;\n\tvar db, main;\n\n\t"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/fetch.js",
    "chars": 5161,
    "preview": "(function() {\n  'use strict';\n\n  if (window.fetch) {\n    return\n  }\n\n  function Headers(headers) {\n    this.map = {}\n\n  "
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/iframe.html",
    "chars": 169,
    "preview": "<!DOCTYPE html>\n<html manifest=\"/offline.appcache\">\n  <head>\n    <title>FT Tech News</title>\n  </head>\n  <body>\n    <scr"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/iframe.js",
    "chars": 1119,
    "preview": "(function() {\n\t\"use strict\";\n\n\tvar checkTimer = null, ac = window.applicationCache, status = null, hasChecked = false, l"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/promise.js",
    "chars": 5193,
    "preview": "!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/styles.css",
    "chars": 406,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family: helvetica, sans-serif;\n}\n* {\n  box-sizing: border-box;\n}\nh1 {\n  padding"
  },
  {
    "path": "05-offline-news/04-more-hacking-appcache/public/templates.js",
    "chars": 569,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tdata = data || [];\n\t\tvar u"
  },
  {
    "path": "05-offline-news/05-success/README.md",
    "chars": 873,
    "preview": "# Success!\n\nWe have achieved the goal of creating an online-first news app that works on all major browsers and has a un"
  },
  {
    "path": "05-offline-news/README.md",
    "chars": 1400,
    "preview": "# Offline news website\n\nWe’re going to make a simple offline-first to-do application with HTML5 technology. Here is what"
  },
  {
    "path": "06-offline-news-with-service-worker/01-scaffolding/README.md",
    "chars": 4262,
    "preview": "# Scaffolding\n\nWe're going to go back to the simple implementation of the online only news app we built [at the beginnin"
  },
  {
    "path": "06-offline-news-with-service-worker/01-scaffolding/index.js",
    "chars": 1059,
    "preview": "var port = Number(process.env.PORT || 8080);\nvar api = 'https://offline-news-api.herokuapp.com/stories';\nvar express = r"
  },
  {
    "path": "06-offline-news-with-service-worker/01-scaffolding/package.json",
    "chars": 116,
    "preview": "{\n  \"dependencies\": {\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.10.0\",\n    \"isomorphic-fetch\": \"^1.0.0\"\n  }\n}\n"
  },
  {
    "path": "06-offline-news-with-service-worker/01-scaffolding/public/styles.css",
    "chars": 390,
    "preview": "body {\n\tmargin: 0;\n\tpadding: 0;\n\tfont-family: helvetica, sans-serif;\n}\n* {\n\tbox-sizing: border-box;\n}\nh1 {\n\tpadding: 14p"
  },
  {
    "path": "06-offline-news-with-service-worker/01-scaffolding/public/templates.js",
    "chars": 1131,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tvar ul = '';\n\t\tdata.forEac"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/README.md",
    "chars": 3138,
    "preview": "# Registering a Service Worker\n\n## Enabling Service Workers\n\nService Workers are not enabled by default in web browsers "
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/index.js",
    "chars": 1059,
    "preview": "var port = Number(process.env.PORT || 8080);\nvar api = 'https://offline-news-api.herokuapp.com/stories';\nvar express = r"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/package.json",
    "chars": 116,
    "preview": "{\n  \"dependencies\": {\n    \"es6-promise\": \"^2.0.0\",\n    \"express\": \"^4.10.0\",\n    \"isomorphic-fetch\": \"^1.0.0\"\n  }\n}\n"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/public/application.js",
    "chars": 118,
    "preview": "(function() {\n\tif ('serviceWorker' in navigator) {\n\t\tnavigator.serviceWorker.register('/service-worker.js');\n\t}\n}());\n"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/public/service-worker.js",
    "chars": 173,
    "preview": "this.oninstall = function(event) {\n\t// TODO: Something interesting\n\tconsole.log(\"The Service Worker has been installed\")"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/public/styles.css",
    "chars": 390,
    "preview": "body {\n\tmargin: 0;\n\tpadding: 0;\n\tfont-family: helvetica, sans-serif;\n}\n* {\n\tbox-sizing: border-box;\n}\nh1 {\n\tpadding: 14p"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/public/templates.js",
    "chars": 1131,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tvar ul = '';\n\t\tdata.forEac"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/styles.css",
    "chars": 390,
    "preview": "body {\n\tmargin: 0;\n\tpadding: 0;\n\tfont-family: helvetica, sans-serif;\n}\n* {\n\tbox-sizing: border-box;\n}\nh1 {\n\tpadding: 14p"
  },
  {
    "path": "06-offline-news-with-service-worker/02-registering-a-service-worker/templates.js",
    "chars": 1131,
    "preview": "(function() {\n\tvar exports = {\n\t\tlist: list,\n\t\tarticle: article\n\t};\n\n\tfunction list(data) {\n\t\tvar ul = '';\n\t\tdata.forEac"
  },
  {
    "path": "06-offline-news-with-service-worker/03-service-worker-caches/README.md",
    "chars": 2380,
    "preview": "# Sevice Worker Caches\n\n## One tiny problem\n\nService Workers will be really helpful for creating offline apps.  Unfortun"
  },
  {
    "path": "06-offline-news-with-service-worker/04-success/README.md",
    "chars": 100,
    "preview": "# Success!\n\n## Exercises\n\n- Rebuild the todo app (either with or without sync) with Service Worker.\n"
  },
  {
    "path": "06-offline-news-with-service-worker/README.md",
    "chars": 2817,
    "preview": "# Offline news with Service Worker\n\nTo get AppCache to work with our offline news website required us to write so many a"
  },
  {
    "path": "07-dexie/README.md",
    "chars": 1546,
    "preview": "# Introducing [Dexie.js](https://github.com/dfahlander/Dexie.js)\n\n[Dexie.js](https://github.com/dfahlander/Dexie.js) is "
  },
  {
    "path": "08-success/README.md",
    "chars": 634,
    "preview": "# Success\n\n## Summary\n\nOver the course of today we've covered **IndexedDB**, **AppCache** and **ServiceWorker** in depth"
  },
  {
    "path": "README.md",
    "chars": 3054,
    "preview": "Making it work offline\n======================\n\nMaking it work offline, the workshop.\n\nI ran this workshop at [SmashingCo"
  },
  {
    "path": "package.json",
    "chars": 1001,
    "preview": "{\n  \"name\": \"workshop-making-it-work-offline\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Making it work offline =========="
  }
]

About this extraction

This page contains the full source code of the matthew-andrews/workshop-making-it-work-offline GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 136 files (277.6 KB), approximately 82.2k tokens, and a symbol index with 545 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!