From 6b5942b59ac157e67a07113b8c0ae91431e994d2 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Mon, 13 Sep 2021 11:19:11 +1000 Subject: [PATCH] btc-wallet: update build, fix routing --- pkg/btc-wallet/config/webpack.dev.js | 37 ++- pkg/btc-wallet/config/webpack.prod.js | 11 +- pkg/btc-wallet/public/index.html | 30 ++ pkg/btc-wallet/src/index.js | 4 +- pkg/btc-wallet/src/js/channel.js | 290 ++++++++++++++++++ pkg/btc-wallet/src/js/components/lib/body.js | 4 +- .../src/js/components/lib/header.js | 2 +- pkg/btc-wallet/src/js/components/root.js | 2 +- pkg/btc-wallet/src/js/store.js | 4 +- pkg/btc-wallet/src/js/subscription.js | 1 + 10 files changed, 357 insertions(+), 28 deletions(-) create mode 100644 pkg/btc-wallet/public/index.html create mode 100644 pkg/btc-wallet/src/js/channel.js diff --git a/pkg/btc-wallet/config/webpack.dev.js b/pkg/btc-wallet/config/webpack.dev.js index 368de7717..532f84974 100644 --- a/pkg/btc-wallet/config/webpack.dev.js +++ b/pkg/btc-wallet/config/webpack.dev.js @@ -1,6 +1,6 @@ const path = require('path'); const webpack = require('webpack'); -// const HtmlWebpackPlugin = require('html-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); // const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const urbitrc = require('./urbitrc'); const fs = require('fs-extra'); @@ -33,6 +33,7 @@ let devServer = { host: '0.0.0.0', disableHostCheck: true, historyApiFallback: true, + publicPath: '/apps/bitcoin/', }; const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`); @@ -40,22 +41,19 @@ const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost: if(urbitrc.URL) { devServer = { ...devServer, - index: '', - proxy: { - '/~btc/js/bundle/index.*.js': { - target: 'http://localhost:9000', - pathRewrite: (req, path) => { - return '/index.js' + index: 'index.html', + proxy: [{ + target: 'http://localhost:9000', + changeOrigin: true, + target: urbitrc.URL, + router, + context: path => { + if(path === '/apps/bitcoin/desk.js') { + return true; } - }, - '**': { - changeOrigin: true, - target: urbitrc.URL, - router, - // ensure proxy doesn't timeout channels - proxyTimeout: 0 + return !path.startsWith('/apps/bitcoin') } - } + }] }; } @@ -107,7 +105,12 @@ module.exports = { devtool: 'inline-source-map', devServer: devServer, plugins: [ - new UrbitShipPlugin(urbitrc) + new UrbitShipPlugin(urbitrc), + new HtmlWebpackPlugin({ + title: 'Bitcoin Wallet', + template: './public/index.html' + }) + ], watch: true, watchOptions: { @@ -118,7 +121,7 @@ module.exports = { filename: 'index.js', chunkFilename: 'index.js', path: path.resolve(__dirname, '../dist'), - publicPath: '/', + publicPath: '/apps/bitcoin/', globalObject: 'this' }, optimization: { diff --git a/pkg/btc-wallet/config/webpack.prod.js b/pkg/btc-wallet/config/webpack.prod.js index ed7403784..7f184e547 100644 --- a/pkg/btc-wallet/config/webpack.prod.js +++ b/pkg/btc-wallet/config/webpack.prod.js @@ -1,5 +1,6 @@ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); // const urbitrc = require('./urbitrc'); module.exports = { @@ -44,14 +45,18 @@ module.exports = { }, devtool: 'source-map', plugins: [ - new CleanWebpackPlugin() + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + title: 'Bitcoin Wallet', + template: './public/index.html' + }) ], output: { filename: (pathData) => { return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js'; }, - path: path.resolve(__dirname, `../../arvo/app/btc-wallet/js/bundle`), - publicPath: '/', + path: path.resolve(__dirname, 'dist'), + publicPath: '/apps/bitcoin/', }, optimization: { minimize: true, diff --git a/pkg/btc-wallet/public/index.html b/pkg/btc-wallet/public/index.html new file mode 100644 index 000000000..059cc631d --- /dev/null +++ b/pkg/btc-wallet/public/index.html @@ -0,0 +1,30 @@ + + + + Wallet + + + + + + + + + +
+
+ + + + diff --git a/pkg/btc-wallet/src/index.js b/pkg/btc-wallet/src/index.js index d4852ef26..5aa431a92 100644 --- a/pkg/btc-wallet/src/index.js +++ b/pkg/btc-wallet/src/index.js @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Root } from './js/components/root.js'; import { api } from './js/api.js'; -import { subscription } from "./js/subscription.js"; +import Channel from './js/channel'; import './css/indigo-static.css'; import './css/fonts.css'; @@ -10,7 +10,7 @@ import './css/custom.css'; // rebuild x3 -const channel = new window.channel(); +const channel = new Channel(); api.setChannel(window.ship, channel); diff --git a/pkg/btc-wallet/src/js/channel.js b/pkg/btc-wallet/src/js/channel.js new file mode 100644 index 000000000..48d7e62ef --- /dev/null +++ b/pkg/btc-wallet/src/js/channel.js @@ -0,0 +1,290 @@ +export default class Channel { + constructor() { + this.init(); + this.deleteOnUnload(); + + // a way to handle channel errors + // + // + this.onChannelError = (err) => { + console.error('event source error: ', err); + }; + this.onChannelOpen = (e) => { + console.log('open', e); + }; + } + + init() { + this.debounceInterval = 500; + // unique identifier: current time and random number + // + this.uid = + new Date().getTime().toString() + + "-" + + Math.random().toString(16).slice(-6); + + this.requestId = 1; + + // the currently connected EventSource + // + this.eventSource = null; + + // the id of the last EventSource event we received + // + this.lastEventId = 0; + + // this last event id acknowledgment sent to the server + // + this.lastAcknowledgedEventId = 0; + + // a registry of requestId to successFunc/failureFunc + // + // These functions are registered during a +poke and are executed + // in the onServerEvent()/onServerError() callbacks. Only one of + // the functions will be called, and the outstanding poke will be + // removed after calling the success or failure function. + // + + this.outstandingPokes = new Map(); + + // a registry of requestId to subscription functions. + // + // These functions are registered during a +subscribe and are + // executed in the onServerEvent()/onServerError() callbacks. The + // event function will be called whenever a new piece of data on this + // subscription is available, which may be 0, 1, or many times. The + // disconnect function may be called exactly once. + // + this.outstandingSubscriptions = new Map(); + + this.outstandingJSON = []; + + this.debounceTimer = null; + } + + resetDebounceTimer() { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } + this.debounceTimer = setTimeout(() => { + this.sendJSONToChannel(); + }, this.debounceInterval) + } + + setOnChannelError(onError = (err) => {}) { + this.onChannelError = onError; + } + + setOnChannelOpen(onOpen = (e) => {}) { + this.onChannelOpen = onOpen; + } + + deleteOnUnload() { + window.addEventListener("beforeunload", (event) => { + this.delete(); + }); + } + + clearQueue() { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + this.sendJSONToChannel(); + } + + // sends a poke to an app on an urbit ship + // + poke(ship, app, mark, json, successFunc, failureFunc) { + let id = this.nextId(); + this.outstandingPokes.set( + id, + { + success: successFunc, + fail: failureFunc + } + ); + + const j = { + id, + action: "poke", + ship, + app, + mark, + json + }; + + this.sendJSONToChannel(j); + } + + // subscribes to a path on an specific app and ship. + // + // Returns a subscription id, which is the same as the same internal id + // passed to your Urbit. + subscribe( + ship, + app, + path, + connectionErrFunc = () => {}, + eventFunc = () => {}, + quitFunc = () => {}, + subAckFunc = () => {}, + ) { + let id = this.nextId(); + this.outstandingSubscriptions.set( + id, + { + err: connectionErrFunc, + event: eventFunc, + quit: quitFunc, + subAck: subAckFunc + } + ); + + const json = { + id, + action: "subscribe", + ship, + app, + path + } + + this.resetDebounceTimer(); + + this.outstandingJSON.push(json); + return id; + } + + // quit the channel + // + delete() { + let id = this.nextId(); + clearInterval(this.ackTimer); + navigator.sendBeacon(this.channelURL(), JSON.stringify([{ + id, + action: "delete" + }])); + if (this.eventSource) { + this.eventSource.close(); + } + } + + // unsubscribe to a specific subscription + // + unsubscribe(subscription) { + let id = this.nextId(); + this.sendJSONToChannel({ + id, + action: "unsubscribe", + subscription + }); + } + + // sends a JSON command command to the server. + // + sendJSONToChannel(j) { + let req = new XMLHttpRequest(); + req.open("PUT", this.channelURL()); + req.setRequestHeader("Content-Type", "application/json"); + + if (this.lastEventId == this.lastAcknowledgedEventId) { + if (j) { + this.outstandingJSON.push(j); + } + + if (this.outstandingJSON.length > 0) { + let x = JSON.stringify(this.outstandingJSON); + req.send(x); + } + } else { + // we add an acknowledgment to clear the server side queue + // + // The server side puts messages it sends us in a queue until we + // acknowledge that we received it. + // + let payload = [ + ...this.outstandingJSON, + {action: "ack", "event-id": this.lastEventId} + ]; + if (j) { + payload.push(j) + } + let x = JSON.stringify(payload); + req.send(x); + + this.lastAcknowledgedEventId = this.lastEventId; + } + this.outstandingJSON = []; + + this.connectIfDisconnected(); + } + + // connects to the EventSource if we are not currently connected + // + connectIfDisconnected() { + if (this.eventSource) { + return; + } + + this.eventSource = new EventSource(this.channelURL(), {withCredentials:true}); + this.eventSource.onmessage = e => { + this.lastEventId = parseInt(e.lastEventId, 10); + + let obj = JSON.parse(e.data); + let pokeFuncs = this.outstandingPokes.get(obj.id); + let subFuncs = this.outstandingSubscriptions.get(obj.id); + + if (obj.response == "poke" && !!pokeFuncs) { + let funcs = pokeFuncs; + if (obj.hasOwnProperty("ok")) { + funcs["success"](); + } else if (obj.hasOwnProperty("err")) { + funcs["fail"](obj.err); + } else { + console.error("Invalid poke response: ", obj); + } + this.outstandingPokes.delete(obj.id); + + } else if (obj.response == "subscribe" || + (obj.response == "poke" && !!subFuncs)) { + let funcs = subFuncs; + + if (obj.hasOwnProperty("err")) { + funcs["err"](obj.err); + this.outstandingSubscriptions.delete(obj.id); + } else if (obj.hasOwnProperty("ok")) { + funcs["subAck"](obj); + } + } else if (obj.response == "diff") { + // ensure we ack before channel clogs + if((this.lastEventId - this.lastAcknowledgedEventId) > 30) { + this.clearQueue(); + } + + let funcs = subFuncs; + funcs["event"](obj.json); + } else if (obj.response == "quit") { + let funcs = subFuncs; + funcs["quit"](obj); + this.outstandingSubscriptions.delete(obj.id); + } else { + console.log("Unrecognized response: ", e); + } + } + + this.eventSource.onopen = this.onChannelOpen; + + this.eventSource.onerror = e => { + this.delete(); + this.init(); + this.onChannelError(e); + } + } + + channelURL() { + return "/~/channel/" + this.uid; + } + + nextId() { + return this.requestId++; + } +} diff --git a/pkg/btc-wallet/src/js/components/lib/body.js b/pkg/btc-wallet/src/js/components/lib/body.js index eff8c8d8e..0fe1d798d 100644 --- a/pkg/btc-wallet/src/js/components/lib/body.js +++ b/pkg/btc-wallet/src/js/components/lib/body.js @@ -40,7 +40,7 @@ export default class Body extends Component { } else { return ( - + - + + {loaded ? ( diff --git a/pkg/btc-wallet/src/js/store.js b/pkg/btc-wallet/src/js/store.js index 40cd3270f..c10097bcc 100644 --- a/pkg/btc-wallet/src/js/store.js +++ b/pkg/btc-wallet/src/js/store.js @@ -7,7 +7,7 @@ class Store { constructor() { this.state = { loadedBtc: false, - loadedSettings: false, + loadedSettings: true, loaded: false, providerPerms: {}, shipWallets: {}, @@ -23,7 +23,7 @@ class Store { BTC: { last: 1, symbol: 'BTC' } }, denomination: 'BTC', - showWarning: true, + showWarning: false, error: '', broadcastSuccess: false, }; diff --git a/pkg/btc-wallet/src/js/subscription.js b/pkg/btc-wallet/src/js/subscription.js index d83f65d92..10adfe1c6 100644 --- a/pkg/btc-wallet/src/js/subscription.js +++ b/pkg/btc-wallet/src/js/subscription.js @@ -19,6 +19,7 @@ export class Subscription { } initializeSettings() { + return; let app = 'settings-store'; let path = '/bucket/btc-wallet';