From f70e80805687f70180bdf42be2157c89e82e4237 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Tue, 20 Dec 2016 01:55:57 +0100 Subject: [PATCH] Fallback in Contract Dev if no worker --- js/src/redux/providers/compilerActions.js | 9 +- js/src/redux/providers/compilerReducer.js | 2 +- js/src/serviceWorker.js | 192 +++++++----------- js/src/util/solidity.js | 79 +++++++ js/src/views/WriteContract/writeContract.js | 4 +- .../views/WriteContract/writeContractStore.js | 128 +++++++++--- 6 files changed, 257 insertions(+), 157 deletions(-) create mode 100644 js/src/util/solidity.js diff --git a/js/src/redux/providers/compilerActions.js b/js/src/redux/providers/compilerActions.js index 9c4eb9535..d638c03a2 100644 --- a/js/src/redux/providers/compilerActions.js +++ b/js/src/redux/providers/compilerActions.js @@ -23,13 +23,8 @@ let workerRegistration; if ('serviceWorker' in navigator) { workerRegistration = runtime .register() - .then(() => { - console.log('registering service worker'); - return navigator.serviceWorker.ready; - }) + .then(() => navigator.serviceWorker.ready) .then((registration) => { - console.log('registered service worker'); - const _worker = registration.active; _worker.controller = registration.active; const worker = new PromiseWorker(_worker); @@ -68,7 +63,7 @@ export function setupWorker () { }) .catch((error) => { console.error('sw', error); - dispatch(setError(error)); + dispatch(setWorker(null)); }); }; } diff --git a/js/src/redux/providers/compilerReducer.js b/js/src/redux/providers/compilerReducer.js index 7470f0751..e23bf3b16 100644 --- a/js/src/redux/providers/compilerReducer.js +++ b/js/src/redux/providers/compilerReducer.js @@ -17,7 +17,7 @@ import { handleActions } from 'redux-actions'; const initialState = { - worker: null, + worker: undefined, error: null }; diff --git a/js/src/serviceWorker.js b/js/src/serviceWorker.js index a7052ad84..c558a57cf 100644 --- a/js/src/serviceWorker.js +++ b/js/src/serviceWorker.js @@ -14,9 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import solc from 'solc/browser-wrapper'; -// import { isWebUri } from 'valid-url'; import registerPromiseWorker from 'promise-worker/register'; +import SolidityUtils from '~/util/solidity'; const CACHE_NAME = 'parity-cache-v1'; @@ -25,25 +24,71 @@ registerPromiseWorker((msg) => { }); self.addEventListener('install', (event) => { - console.warn('installing sw'); event.waitUntil(self.skipWaiting()); }); self.addEventListener('activate', (event) => { - console.warn('activating sw'); event.waitUntil(self.clients.claim()); }); -self.solcVersions = {}; +self.addEventListener('fetch', (event) => { + const { url } = event.request; + + if (/raw.githubusercontent.com\/ethereum\/solc-bin(.+)list\.json$/.test(url)) { + // Return the cached version, but still update it in background + return event.respondWith(cachedFetcher(event.request, true)); + } + + if (/raw.githubusercontent.com\/ethereum\/solc-bin(.+)soljson(.+)\.js$/.test(url)) { + return event.respondWith(cachedFetcher(event.request)); + } +}); + +self.solc = {}; self.files = {}; +function cachedFetcher (request, update = false) { + return caches + .match(request) + .then((response) => { + // Return cached response if exists and no + // updates needed + if (response && !update) { + return response; + } + + const fetcher = fetch(request.clone()) + .then((response) => { + // Check if we received a valid response + if (!response || response.status !== 200) { + return response; + } + + return caches + .open(CACHE_NAME) + .then((cache) => { + cache.put(request, response.clone()); + return response; + }); + }); + + // Cache hit - return response + // Still want to perform the fetch (update) + if (response) { + return response; + } + + return fetcher; + }); +} + function handleMessage (message) { switch (message.action) { case 'compile': return compile(message.data); case 'load': - return load(message.data); + return getCompiler(message.data).then(() => 'ok'); case 'setFiles': return setFiles(message.data); @@ -54,6 +99,15 @@ function handleMessage (message) { } } +function compile (data) { + const { build } = data; + + return getCompiler(build) + .then((compiler) => { + return SolidityUtils.compile(data, compiler); + }); +} + function setFiles (files) { const prevFiles = self.files; const nextFiles = files.reduce((obj, file) => { @@ -69,120 +123,22 @@ function setFiles (files) { return 'ok'; } -// @todo re-implement find imports (with ASYNC fetch) -// function findImports (path) { -// if (self.files[path]) { -// if (self.files[path].error) { -// return Promise.reject(self.files[path].error); -// } +function getCompiler (build) { + const { longVersion } = build; -// return Promise.resolve(self.files[path]); -// } + const fetcher = (url) => { + const request = new Request(url); + return cachedFetcher(request); + }; -// if (isWebUri(path)) { -// console.log('[sw] fetching', path); - -// return fetch(path) -// .then((r) => r.text()) -// .then((c) => { -// console.log('[sw]', 'got content at ' + path); -// self.files[path] = c; -// return c; -// }) -// .catch((e) => { -// console.error('[sw]', 'fetching', path, e); -// self.files[path] = { error: e }; -// throw e; -// }); -// } - -// console.log(`[sw] path ${path} not found...`); -// return Promise.reject('File not found'); -// } - -function compile (data, optimized = 1) { - const { sourcecode, build } = data; - - return fetchSolidity(build) - .then((compiler) => { - const start = Date.now(); - console.log('[sw] compiling...'); - - const input = { - '': sourcecode - }; - - const compiled = compiler.compile({ sources: input }, optimized); - - const time = Math.round((Date.now() - start) / 100) / 10; - console.log(`[sw] done compiling in ${time}s`); - - compiled.version = build.longVersion; - - return compiled; - }); -} - -function load (build) { - return fetchSolidity(build) - .then(() => 'ok'); -} - -function fetchSolc (build) { - const { path, longVersion } = build; - const URL = `https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/${path}`; - - return caches - .match(URL) - .then((response) => { - if (response) { - return response; - } - - console.log(`[sw] fetching solc-bin ${longVersion} at ${URL}`); - - return fetch(URL) - .then((response) => { - if (!response || response.status !== 200) { - return response; - } - - const responseToCache = response.clone(); - - return caches.open(CACHE_NAME) - .then((cache) => { - return cache.put(URL, responseToCache); - }) - .then(() => { - return response; - }); - }); - }); -} - -function fetchSolidity (build) { - const { path, longVersion } = build; - - if (self.solcVersions[path]) { - return Promise.resolve(self.solcVersions[path]); + if (!self.solc[longVersion]) { + self.solc[longVersion] = SolidityUtils + .getCompiler(build, fetcher) + .then((compiler) => { + self.solc[longVersion] = compiler; + return compiler; + }); } - return fetchSolc(build) - .then((r) => r.text()) - .then((code) => { - const solcCode = code.replace(/^var Module;/, 'var Module=self.__solcModule;'); - self.__solcModule = {}; - - console.log(`[sw] evaluating ${longVersion}`); - - // eslint-disable-next-line no-eval - eval(solcCode); - - console.log(`[sw] done evaluating ${longVersion}`); - - const compiler = solc(self.__solcModule); - self.solcVersions[path] = compiler; - - return compiler; - }); + return self.solc[longVersion]; } diff --git a/js/src/util/solidity.js b/js/src/util/solidity.js new file mode 100644 index 000000000..f09213ac5 --- /dev/null +++ b/js/src/util/solidity.js @@ -0,0 +1,79 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import solc from 'solc/browser-wrapper'; + +export default class SolidityUtils { + + static compile (data, compiler) { + const { sourcecode, build, optimized = 1 } = data; + + const start = Date.now(); + console.log('[solidity] compiling...'); + + const input = { + '': sourcecode + }; + + const compiled = compiler.compile({ sources: input }, optimized); + + const time = Math.round((Date.now() - start) / 100) / 10; + console.log(`[solidity] done compiling in ${time}s`); + + compiled.version = build.longVersion; + compiled.sourcecode = sourcecode; + + return compiled; + } + + static getCompiler (build, _fetcher) { + const { longVersion, path } = build; + + const URL = `https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/${path}`; + + const fetcher = typeof _fetcher === 'function' + ? _fetcher + : (url) => fetch(url); + + const isWorker = typeof window !== 'object'; + + return fetcher(URL) + .then((r) => r.text()) + .then((code) => { + // `window` for main thread, `self` for workers + const _self = isWorker ? self : window; + _self.Module = {}; + + const solcCode = code.replace('var Module;', `var Module=${isWorker ? 'self' : 'window'}.Module;`); + + console.log(`[solidity] evaluating ${longVersion}`); + + try { + // eslint-disable-next-line no-eval + eval(solcCode); + } catch (e) { + return Promise.reject(e); + } + + console.log(`[solidity] done evaluating ${longVersion}`); + + const compiler = solc(_self.Module); + delete _self.Module; + + return compiler; + }); + } +} diff --git a/js/src/views/WriteContract/writeContract.js b/js/src/views/WriteContract/writeContract.js index 2293eff73..5266fb625 100644 --- a/js/src/views/WriteContract/writeContract.js +++ b/js/src/views/WriteContract/writeContract.js @@ -57,7 +57,7 @@ class WriteContract extends Component { const { setupWorker, worker } = this.props; setupWorker(); - if (worker) { + if (worker !== undefined) { this.store.setWorker(worker); } } @@ -77,7 +77,7 @@ class WriteContract extends Component { // Set the worker if not set before (eg. first page loading) componentWillReceiveProps (nextProps) { - if (!this.props.worker && nextProps.worker) { + if (this.props.worker === undefined && nextProps.worker !== undefined) { this.store.setWorker(nextProps.worker); } diff --git a/js/src/views/WriteContract/writeContractStore.js b/js/src/views/WriteContract/writeContractStore.js index 96d9c4d07..3a9a40aab 100644 --- a/js/src/views/WriteContract/writeContractStore.js +++ b/js/src/views/WriteContract/writeContractStore.js @@ -14,11 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { action, observable } from 'mobx'; +import { action, observable, transaction } from 'mobx'; import store from 'store'; import { debounce } from 'lodash'; import { sha3 } from '~/api/util/sha3'; +import SolidityUtils from '~/util/solidity'; const WRITE_CONTRACT_STORE_KEY = '_parity::writeContractStore'; @@ -79,6 +80,9 @@ export default class WriteContractStore { snippets = SNIPPETS; worker = null; + useWorker = true; + solc = {}; + constructor () { this.debouncedCompile = debounce(this.handleCompile, 1000); } @@ -131,6 +135,9 @@ export default class WriteContractStore { this.selectedBuild = latestIndex; return promise; + }) + .catch((error) => { + this.setWorkerError(error); }); } @@ -143,32 +150,71 @@ export default class WriteContractStore { return this.loadSolidityVersion(this.builds[value]); } + getCompiler (build) { + const { longVersion } = build; + + if (!this.solc[longVersion]) { + this.solc[longVersion] = SolidityUtils + .getCompiler(build) + .then((compiler) => { + this.solc[longVersion] = compiler; + return compiler; + }) + .catch((error) => { + this.setWorkerError(error); + }); + } + + return Promise.resolve(this.solc[longVersion]); + } + @action loadSolidityVersion = (build) => { - if (!this.worker) { + if (this.worker === undefined) { return; + } else if (this.worker === null) { + this.useWorker = false; } if (this.loadingSolidity) { return this.loadingSolidity; } - this.loadingSolidity = this.worker - .postMessage({ - action: 'load', - data: build - }) - .then((result) => { - if (result !== 'ok') { - this.setWorkerError(result); - } - }) - .catch((error) => { - this.setWorkerError(error); - }) - .then(() => { - this.loadingSolidity = false; - this.loading = false; - }); + if (this.useWorker) { + this.loadingSolidity = this.worker + .postMessage({ + action: 'load', + data: build + }) + .then((result) => { + if (result !== 'ok') { + throw new Error('error while loading solidity: ' + result); + } + + this.loadingSolidity = false; + this.loading = false; + }) + .catch((error) => { + console.warn('error while loading solidity', error); + this.useWorker = false; + this.loadingSolidity = null; + + return this.loadSolidityVersion(build); + }); + } else { + this.loadingSolidity = this + .getCompiler(build) + .then(() => { + this.loadingSolidity = false; + this.loading = false; + + return 'ok'; + }) + .catch((error) => { + this.setWorkerError(error); + this.loadingSolidity = false; + this.loading = false; + }); + } return this.loadingSolidity; } @@ -202,9 +248,32 @@ export default class WriteContractStore { this.contract = this.contracts[Object.keys(this.contracts)[value]]; } + compile = (data) => { + if (this.useWorker) { + return this.worker.postMessage({ + action: 'compile', + data + }); + } + + return new Promise((resolve, reject) => { + window.setTimeout(() => { + this + .getCompiler(data.build) + .then((compiler) => { + return SolidityUtils.compile(data, compiler); + }) + .then(resolve) + .catch(reject); + }, 0); + }); + } + @action handleCompile = (loadFiles = false) => { - this.compiled = false; - this.compiling = true; + transaction(() => { + this.compiled = false; + this.compiling = true; + }); const build = this.builds[this.selectedBuild]; const version = build.longVersion; @@ -219,19 +288,16 @@ export default class WriteContractStore { resolve(this.lastCompilation); }, 500); }); - } else if (this.worker) { - promise = loadFiles + } else { + promise = loadFiles && this.useWorker ? this.sendFilesToWorker() : Promise.resolve(); promise = promise .then(() => { - return this.worker.postMessage({ - action: 'compile', - data: { - sourcecode: sourcecode, - build: build - } + return this.compile({ + sourcecode: sourcecode, + build: build }); }) .then((data) => { @@ -427,6 +493,10 @@ export default class WriteContractStore { } sendFilesToWorker = () => { + if (!this.useWorker) { + return Promise.resolve(); + } + const files = [].concat( Object.values(this.snippets), Object.values(this.savedContracts)