Repository: bahmutov/bottle-service
Branch: master
Commit: b44a23fbedcc
Files: 14
Total size: 27.5 KB
Directory structure:
gitextract_eg55b8px/
├── .gitignore
├── .npmrc
├── .travis.yml
├── Procfile
├── README.md
├── deploy.json
├── dist/
│ ├── app.css
│ ├── bottle-service.js
│ ├── bottle.js
│ └── index.html
├── package.json
├── src/
│ ├── bottle-service.js
│ └── bottle.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules/
.grunt/
.DS_Store
npm-debug.log
================================================
FILE: .npmrc
================================================
registry=http://registry.npmjs.org/
save-exact=true
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
cache:
directories:
- node_modules
notifications:
email: false
node_js:
- '4'
before_install:
- npm i -g npm@^2.0.0
before_script:
- npm prune
after_success:
- npm run semantic-release
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"
================================================
FILE: Procfile
================================================
web: npm start
================================================
FILE: README.md
================================================
# bottle-service
> Instant web applications restored from ServiceWorker cache
[![NPM][bottle-service-icon] ][bottle-service-url]
[![Build status][bottle-service-ci-image] ][bottle-service-ci-url]
[![semantic-release][semantic-image] ][semantic-url]
[Live demo](https://glebbahmutov.com/bottle-service/) - please use Chrome or Opera Desktop
[Instant app demo](https://instant-todo.herokuapp.com/) - TodoMVC that is instant on
page reload, hosted on free Heroku dyno, and needs Chrome browser for now

## Browser support
### Chrome
* Nothing to do, `ServiceWorker` should be enabled by default
### Firefox
* Open `about:config`
* Set the `dom.serviceWorkers.enabled` setting to **true**
* Set the `dom.serviceWorkers.interception.enabled` setting to **true**
## Api
This library attaches itself as `bottleService` object to the 'window'. Every time
you want to store HTML snapshot for an element with id 'myApp', call
```js
// "my-app-name" will be used in the future to allow separate parts of the
// page to be saved separately
//
'
var startIndex = pageHtml.indexOf(toReplaceStart)
var finishIndex = pageHtml.indexOf(toReplaceFinish)
if (startIndex !== -1 && finishIndex > startIndex) {
console.log('found fragment')
pageHtml = pageHtml.substr(0, startIndex + toReplaceStart.length) +
'\n' + contents.html + '\n' +
pageHtml.substr(finishIndex)
}
// console.log('page html')
// console.log(pageHtml)
var responseOptions = {
status: 200,
headers: {
'Content-Type': 'text/html charset=UTF-8'
}
}
return new Response(pageHtml, responseOptions)
})
} else {
return response
}
}, function notFound () {
return response
})
})
)
})
// use window.navigator.serviceWorker.controller.postMessage('hi')
// to communicate with this service worker
self.onmessage = function onMessage (event) {
console.log('message to bottle-service worker cmd', event.data && event.data.cmd)
// TODO how to use application name?
dataStore().then(function (store) {
switch (event.data.cmd) {
case 'print': {
return store.getItem('contents')
.then(function (res) {
console.log('bottle service has contents')
console.log(res)
})
}
case 'clear': {
console.log('clearing the bottle')
return store.setItem('contents', {})
}
case 'refill': {
return store.setItem('contents', {
html: event.data.html,
id: event.data.id
}).then(function () {
console.log('saved new html for id', event.data.id)
})
}
default: {
console.error(myName, 'unknown command', event.data)
}
}
})
}
/***/ },
/* 1 */
/***/ function(module, exports) {
// Poor man's async "localStorage" on top of Cache
// https://developer.mozilla.org/en-US/docs/Web/API/Cache
if (typeof caches === 'undefined') {
throw new Error('Cannot find object caches?! Cannot init cache-storage')
}
/* global caches, Response */
function dataStore (name) {
var id = name ? name + '-v1' : 'cache-storage-v1'
return caches.open(id)
.then(function (cache) {
return {
setItem: function (key, data) {
return cache.put(key, new Response(JSON.stringify(data)))
},
getItem: function (key) {
return cache.match(key)
.then(function (res) {
return res &&
res.text().then(JSON.parse)
})
}
}
})
}
module.exports = dataStore
/***/ }
/******/ ]);
================================================
FILE: dist/bottle.js
================================================
!(function startBottleService (root) {
'use strict'
if (!root.navigator) {
console.error('Missing navigator')
return
}
if (!root.navigator.serviceWorker) {
console.error('Sorry, not ServiceWorker feature, maybe enable it?')
console.error('http://jakearchibald.com/2014/using-serviceworker-today/')
return
}
// TODO package lazy-ass and check-more-types using webpack
function toString (x) {
return typeof x === 'string' ? x : JSON.stringify(x)
}
function la (condition) {
if (!condition) {
var args = Array.prototype.slice.call(arguments, 1)
.map(toString)
throw new Error(args.join(' '))
}
}
function isFunction (f) {
return typeof f === 'function'
}
function getCurrentScriptFolder () {
var scriptEls = document.getElementsByTagName('script')
var thisScriptEl = scriptEls[scriptEls.length - 1]
var scriptPath = thisScriptEl.src
return scriptPath.substr(0, scriptPath.lastIndexOf('/') + 1)
}
var serviceScriptUrl = getCurrentScriptFolder() + 'bottle-service.js'
// assume we are running at /pathname
var scope = window.location.pathname
var send = function mockSend () {
console.error('Bottle service not initialized yet')
}
function registeredWorker (registration) {
la(registration, 'missing service worker registration')
la(registration.active, 'missing active service worker')
la(isFunction(registration.active.postMessage),
'expected function postMessage to communicate with service worker')
send = registration.active.postMessage.bind(registration.active)
var info = '\nbottle-service - .\n' +
'I have a valid service-turtle, use `bottleService` object to update cached page'
console.log(info)
registration.active.onmessage = function messageFromServiceWorker (e) {
console.log('received message from the service worker', e)
}
}
function onError (err) {
if (err.message.indexOf('missing active') !== -1) {
// the service worker is installed
window.location.reload()
} else {
console.error('bottle service error', err)
}
}
root.navigator.serviceWorker.register(serviceScriptUrl, { scope: scope })
.then(registeredWorker)
.catch(onError)
root.bottleService = {
refill: function refill (applicationName, id) {
console.log('bottle-service: html for app %s element %s', applicationName, id)
var el = document.getElementById(id)
la(el, 'could not find element with id', id)
var html = el.innerHTML.trim()
send({
cmd: 'refill',
html: html,
name: applicationName,
id: id
})
},
print: function print (applicationName) {
send({
cmd: 'print',
name: applicationName
})
},
clear: function clear (applicationName) {
send({
cmd: 'clear',
name: applicationName
})
}
}
}(window))
================================================
FILE: dist/index.html
================================================
bottle-service
bottle-service (tested on Chrome Desktop only)
This page can rewrite itself inside a ServiceWorker behind this website.
Try adding a couple of DOM nodes using a button below, then reload the page.
Notice that the page arrives with new DOM nodes to the browser,
thus there is no blinking / initial pause. If you inspect the fetched page's source
in the Network tab you will see the updated HTML on each reload.
The self-rewriting portion is below
some content here already
Controls
"Add" - inserts a new item into the rewritable portion of this page. Stores the dynamic
HTML portion in the ServiceWorker for the future reload
"Print" - prints what is currently stored for the rewritable portion of the page in the
ServiceWorker (open browser console to view)
"Clear" - clears the stored HTML from the ServiceWorker storage
How to use
Load the page first - I assume you have already done this, if you are reading this
Click "Add ..." button several times, you should see new items in the list.
Reload the page - you should see the items you have added right away - they are part
of the loaded page, not added dynamically later. You can confirm
this by inspecting the downloaded page's source or the contents of the page in the Network tab.
Note
Open browser console to see any messages / errors
================================================
FILE: package.json
================================================
{
"name": "bottle-service",
"description": "Instant web applications restored from ServiceWorker cache",
"main": "index.js",
"version": "0.0.0-semantic-release",
"files": [
"dist/*.js"
],
"scripts": {
"test": "npm run lint",
"lint": "standard src/*.js",
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
"build": "cp src/bottle.js dist/ && npm run webpack",
"webpack": "webpack",
"deploy": "grunty grunt-gh-pages gh-pages deploy.json",
"start": "http-server dist -c-1",
"dev-start": "http-server dist -c-1 -p 3006",
"commit": "commit-wizard",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"issues": "git-issues"
},
"repository": {
"type": "git",
"url": "https://github.com/bahmutov/bottle-service.git"
},
"keywords": [
"service",
"worker",
"serviceWorker",
"cache",
"hydrate",
"web",
"performance"
],
"author": "Gleb Bahmutov ",
"license": "MIT",
"bugs": {
"url": "https://github.com/bahmutov/bottle-service/issues"
},
"homepage": "https://github.com/bahmutov/bottle-service#readme",
"dependencies": {
"caches-storage": "1.1.0",
"http-server": "0.8.5"
},
"devDependencies": {
"git-issues": "1.2.0",
"grunt": "0.4.5",
"grunt-gh-pages": "1.0.0",
"grunty": "0.2.0",
"pre-git": "3.1.1",
"semantic-release": "^4.3.5",
"standard": "5.4.1",
"webpack": "1.12.9"
},
"private": false,
"config": {
"pre-git": {
"commit-msg": [
"simple"
],
"pre-commit": [
"npm run lint"
],
"pre-push": [
"npm run size"
],
"post-commit": [],
"post-merge": []
}
}
}
================================================
FILE: src/bottle-service.js
================================================
'use strict'
/*
This is ServiceWorker code
*/
/* global self, Response, Promise, location, fetch */
var myName = 'bottle-service'
console.log(myName, 'startup')
function dataStore () {
var cachesStorage = require('caches-storage')
return cachesStorage(myName)
}
self.addEventListener('install', function (event) {
console.log(myName, 'installed')
})
self.addEventListener('activate', function () {
console.log(myName, 'activated')
})
var baseHref = location.href.substr(0, location.href.indexOf('bottle-service.js'))
function isIndexPageRequest (event) {
return event &&
event.request &&
event.request.url === baseHref
}
self.addEventListener('fetch', function (event) {
if (!isIndexPageRequest(event)) {
return fetch(event.request)
}
console.log(myName, 'fetching index page', event.request.url)
event.respondWith(
fetch(event.request)
.then(function (response) {
return dataStore()
.then(function (store) {
return store.getItem('contents')
})
.then(function (contents) {
if (contents && contents.html && contents.id) {
console.log('fetched latest', response.url, 'need to update')
console.log('element "%s" with html "%s" ...',
contents.id, contents.html.substr(0, 15))
var copy = response.clone()
return copy.text().then(function (pageHtml) {
console.log('inserting our html')
// HACK using id in the CLOSING TAG to find fragment
var toReplaceStart = '
'
var toReplaceFinish = '
'
var startIndex = pageHtml.indexOf(toReplaceStart)
var finishIndex = pageHtml.indexOf(toReplaceFinish)
if (startIndex !== -1 && finishIndex > startIndex) {
console.log('found fragment')
pageHtml = pageHtml.substr(0, startIndex + toReplaceStart.length) +
'\n' + contents.html + '\n' +
pageHtml.substr(finishIndex)
}
// console.log('page html')
// console.log(pageHtml)
var responseOptions = {
status: 200,
headers: {
'Content-Type': 'text/html charset=UTF-8'
}
}
return new Response(pageHtml, responseOptions)
})
} else {
return response
}
}, function notFound () {
return response
})
})
)
})
// use window.navigator.serviceWorker.controller.postMessage('hi')
// to communicate with this service worker
self.onmessage = function onMessage (event) {
console.log('message to bottle-service worker cmd', event.data && event.data.cmd)
// TODO how to use application name?
dataStore().then(function (store) {
switch (event.data.cmd) {
case 'print': {
return store.getItem('contents')
.then(function (res) {
console.log('bottle service has contents')
console.log(res)
})
}
case 'clear': {
console.log('clearing the bottle')
return store.setItem('contents', {})
}
case 'refill': {
return store.setItem('contents', {
html: event.data.html,
id: event.data.id
}).then(function () {
console.log('saved new html for id', event.data.id)
})
}
default: {
console.error(myName, 'unknown command', event.data)
}
}
})
}
================================================
FILE: src/bottle.js
================================================
!(function startBottleService (root) {
'use strict'
if (!root.navigator) {
console.error('Missing navigator')
return
}
if (!root.navigator.serviceWorker) {
console.error('Sorry, not ServiceWorker feature, maybe enable it?')
console.error('http://jakearchibald.com/2014/using-serviceworker-today/')
return
}
// TODO package lazy-ass and check-more-types using webpack
function toString (x) {
return typeof x === 'string' ? x : JSON.stringify(x)
}
function la (condition) {
if (!condition) {
var args = Array.prototype.slice.call(arguments, 1)
.map(toString)
throw new Error(args.join(' '))
}
}
function isFunction (f) {
return typeof f === 'function'
}
function getCurrentScriptFolder () {
var scriptEls = document.getElementsByTagName('script')
var thisScriptEl = scriptEls[scriptEls.length - 1]
var scriptPath = thisScriptEl.src
return scriptPath.substr(0, scriptPath.lastIndexOf('/') + 1)
}
var serviceScriptUrl = getCurrentScriptFolder() + 'bottle-service.js'
// assume we are running at /pathname
var scope = window.location.pathname
var send = function mockSend () {
console.error('Bottle service not initialized yet')
}
function registeredWorker (registration) {
la(registration, 'missing service worker registration')
la(registration.active, 'missing active service worker')
la(isFunction(registration.active.postMessage),
'expected function postMessage to communicate with service worker')
send = registration.active.postMessage.bind(registration.active)
var info = '\nbottle-service - .\n' +
'I have a valid service-turtle, use `bottleService` object to update cached page'
console.log(info)
registration.active.onmessage = function messageFromServiceWorker (e) {
console.log('received message from the service worker', e)
}
}
function onError (err) {
if (err.message.indexOf('missing active') !== -1) {
// the service worker is installed
window.location.reload()
} else {
console.error('bottle service error', err)
}
}
root.navigator.serviceWorker.register(serviceScriptUrl, { scope: scope })
.then(registeredWorker)
.catch(onError)
root.bottleService = {
refill: function refill (applicationName, id) {
console.log('bottle-service: html for app %s element %s', applicationName, id)
var el = document.getElementById(id)
la(el, 'could not find element with id', id)
var html = el.innerHTML.trim()
send({
cmd: 'refill',
html: html,
name: applicationName,
id: id
})
},
print: function print (applicationName) {
send({
cmd: 'print',
name: applicationName
})
},
clear: function clear (applicationName) {
send({
cmd: 'clear',
name: applicationName
})
}
}
}(window))
================================================
FILE: webpack.config.js
================================================
module.exports = {
output: {
path: './dist',
filename: 'bottle-service.js'
},
entry: {
library: './src/bottle-service'
}
}