mirror of https://github.com/vector-im/riot-web
				
				
				
			Move codebase into riot-web
							parent
							
								
									3e77418fd7
								
							
						
					
					
						commit
						4110e2dfa3
					
				|  | @ -0,0 +1,107 @@ | |||
| /* | ||||
|  * Quick-n-dirty algebraic datatypes. | ||||
|  * | ||||
|  * These let us handle the possibility of failure without having to constantly write code to check for it. | ||||
|  * We can apply all of the transformations we need as if the data is present using `map`. | ||||
|  * If there's a None, or a FetchError, or a Pending, those are left untouched. | ||||
|  * | ||||
|  * I've used perhaps an odd bit of terminology from scalaz in `fold`.  This is basically a `switch` statement: | ||||
|  * You pass it a set of functions to handle the various different states of the datatype, and if it finds the | ||||
|  * function it'll call it on its value. | ||||
|  * | ||||
|  * It's handy to have this in functional style when dealing with React as we can dispatch different ways of rendering | ||||
|  * really simply: | ||||
|  * ``` | ||||
|  * bundleFetchStatus.fold({ | ||||
|  *     some: (fetchStatus) => <ProgressBar fetchsStatus={fetchStatus} />, | ||||
|  * }), | ||||
|  * ``` | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| class Optional { | ||||
|     static from(value) { | ||||
|         return value && Some.of(value) || None; | ||||
|     } | ||||
|     map(f) { | ||||
|         return this; | ||||
|     } | ||||
|     flatMap(f) { | ||||
|         return this; | ||||
|     } | ||||
|     fold({ none }) { | ||||
|         return none && none(); | ||||
|     } | ||||
| } | ||||
| class Some extends Optional { | ||||
|     constructor(value) { | ||||
|         super(); | ||||
|         this.value = value; | ||||
|     } | ||||
|     map(f) { | ||||
|         return Some.of(f(this.value)); | ||||
|     } | ||||
|     flatMap(f) { | ||||
|         return f(this.value); | ||||
|     } | ||||
|     fold({ some }) { | ||||
|         return some && some(this.value); | ||||
|     } | ||||
|     static of(value) { | ||||
|         return new Some(value); | ||||
|     } | ||||
| } | ||||
| const None = new Optional(); | ||||
| 
 | ||||
| class FetchStatus { | ||||
|     constructor(opt = {}) { | ||||
|         this.opt = { at: Date.now(), ...opt }; | ||||
|     } | ||||
|     map(f) { | ||||
|         return this; | ||||
|     } | ||||
|     flatMap(f) { | ||||
|         return this; | ||||
|     } | ||||
| } | ||||
| class Success extends FetchStatus { | ||||
|     static of(value) { | ||||
|         return new Success(value); | ||||
|     } | ||||
|     constructor(value, opt) { | ||||
|         super(opt); | ||||
|         this.value = value; | ||||
|     } | ||||
|     map(f) { | ||||
|         return new Success(f(this.value), this.opt); | ||||
|     } | ||||
|     flatMap(f) { | ||||
|         return f(this.value, this.opt); | ||||
|     } | ||||
|     fold({ success }) { | ||||
|         return success instanceof Function ? success(this.value, this.opt) : undefined; | ||||
|     } | ||||
| } | ||||
| class Pending extends FetchStatus { | ||||
|     static of(opt) { | ||||
|         return new Pending(opt); | ||||
|     } | ||||
|     constructor(opt) { | ||||
|         super(opt); | ||||
|     } | ||||
|     fold({ pending }) { | ||||
|         return pending instanceof Function ? pending(this.opt) : undefined; | ||||
|     } | ||||
| } | ||||
| class FetchError extends FetchStatus { | ||||
|     static of(reason, opt) { | ||||
|         return new FetchError(reason, opt); | ||||
|     } | ||||
|     constructor(reason, opt) { | ||||
|         super(opt); | ||||
|         this.reason = reason; | ||||
|     } | ||||
|     fold({ error }) { | ||||
|         return error instanceof Function ? error(this.reason, this.opt) : undefined; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,319 @@ | |||
| class StartupError extends Error {} | ||||
| 
 | ||||
| /* | ||||
|  * We need to know the bundle path before we can fetch the sourcemap files.  In a production environment, we can guess | ||||
|  * it using this. | ||||
|  */ | ||||
| async function getBundleName() { | ||||
|     const res = await fetch("../index.html"); | ||||
|     if (!res.ok) { | ||||
|         throw new StartupError(`Couldn't fetch index.html to prefill bundle; ${res.status} ${res.statusText}`); | ||||
|     } | ||||
|     const index = await res.text(); | ||||
|     return index.split("\n").map((line) => | ||||
|         line.match(/<script src="bundles\/([^/]+)\/bundle.js"/), | ||||
|     ) | ||||
|     .filter((result) => result) | ||||
|     .map((result) => result[1])[0]; | ||||
| } | ||||
| 
 | ||||
| function validateBundle(value) { | ||||
|     return value.match(/^[0-9a-f]{20}$/) ? Some.of(value) : None; | ||||
| } | ||||
| 
 | ||||
| /* A custom fetcher that abandons immediately upon getting a response. | ||||
|  * The purpose of this is just to validate that the user entered a real bundle, and provide feedback. | ||||
|  */ | ||||
| const bundleCache = new Map(); | ||||
| function bundleSubject(bundle) { | ||||
|     if (!bundle.match(/^[0-9a-f]{20}$/)) throw new Error("Bad input"); | ||||
|     if (bundleCache.has(bundle)) { | ||||
|         return bundleCache.get(bundle); | ||||
|     } | ||||
|     const fetcher = new rxjs.BehaviorSubject(Pending.of()); | ||||
|     bundleCache.set(bundle, fetcher); | ||||
| 
 | ||||
|     fetch(`/bundles/${bundle}/bundle.js.map`).then((res) => { | ||||
|         res.body.cancel(); /* Bail on the download immediately - it could be big! */ | ||||
|         const status = res.ok; | ||||
|         if (status) { | ||||
|             fetcher.next(Success.of()); | ||||
|         } else { | ||||
|             fetcher.next(FetchError.of(`Failed to fetch: ${res.status} ${res.statusText}`)); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     return fetcher; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Convert a ReadableStream of bytes into an Observable of a string | ||||
|  * The observable will emit a stream of Pending objects and will concatenate | ||||
|  * the number of bytes received with whatever pendingContext has been supplied. | ||||
|  * Finally, it will emit a Success containing the result. | ||||
|  * You'd use this on a Response.body. | ||||
|  */ | ||||
| function observeReadableStream(readableStream, pendingContext = {}) { | ||||
|     let bytesReceived = 0; | ||||
|     let buffer = ""; | ||||
|     const pendingSubject = new rxjs.BehaviorSubject(Pending.of({ ...pendingContext, bytesReceived })); | ||||
|     const throttledPending = pendingSubject.pipe(rxjs.operators.throttleTime(100)); | ||||
|     const resultObservable = new rxjs.Subject(); | ||||
|     const reader = readableStream.getReader(); | ||||
|     const utf8Decoder = new TextDecoder("utf-8"); | ||||
|     function readNextChunk() { | ||||
|         reader.read().then(({ done, value }) => { | ||||
|             if (done) { | ||||
|                 pendingSubject.complete(); | ||||
|                 resultObservable.next(Success.of(buffer)); | ||||
|                 return; | ||||
|             } | ||||
|             bytesReceived += value.length; | ||||
|             pendingSubject.next(Pending.of({...pendingContext, bytesReceived })); | ||||
|             /* string concatenation is apparently the most performant way to do this */ | ||||
|             buffer += utf8Decoder.decode(value); | ||||
|             readNextChunk(); | ||||
|         }); | ||||
|     } | ||||
|     readNextChunk(); | ||||
|     return rxjs.concat(throttledPending, resultObservable); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * A wrapper which converts the browser's `fetch()` mechanism into an Observable.  The Observable then provides us with | ||||
|  * a stream of datatype values: first, a sequence of Pending objects that keep us up to date with the download progress, | ||||
|  * finally followed by either a Success or Failure object.  React then just has to render each of these appropriately. | ||||
|  */ | ||||
| const fetchCache = new Map(); | ||||
| function fetchAsSubject(endpoint) { | ||||
|     if (fetchCache.has(endpoint)) { | ||||
|         // TODO: expiry/retry logic here?
 | ||||
|         return fetchCache.get(endpoint); | ||||
|     } | ||||
|     const fetcher = new rxjs.BehaviorSubject(Pending.of()); | ||||
|     fetchCache.set(endpoint, fetcher); | ||||
| 
 | ||||
|     fetch(endpoint).then((res) => { | ||||
|         if (!res.ok) { | ||||
|             fetcher.next(FetchError.of(`Failed to fetch endpoint ${endpoint}: ${res.status} ${res.statusText}`)); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const contentLength = res.headers.get("content-length"); | ||||
|         const context = contentLength ? { length: parseInt(contentLength) } : {}; | ||||
| 
 | ||||
|         const streamer = observeReadableStream(res.body, context, endpoint); | ||||
|         streamer.subscribe((value) => { | ||||
|             fetcher.next(value); | ||||
|         }); | ||||
|     }); | ||||
|     return fetcher; | ||||
| } | ||||
| 
 | ||||
| /* ===================== */ | ||||
| /* ==== React stuff ==== */ | ||||
| /* ===================== */ | ||||
| /* Rather than importing an entire build infrastructure, for now we just use React without JSX */ | ||||
| const e = React.createElement; | ||||
| 
 | ||||
| /* | ||||
|  * Provides user feedback given a FetchStatus object. | ||||
|  */ | ||||
| function ProgressBar({ fetchStatus }) { | ||||
|     return e('span', { className: "progress "}, | ||||
|         fetchStatus.fold({ | ||||
|             pending: ({ bytesReceived, length }) => { | ||||
|                 if (!bytesReceived) { | ||||
|                     return e('span', { className: "spinner" }, "\u29b5"); | ||||
|                 } | ||||
|                 const kB = Math.floor(10 * bytesReceived / 1024) / 10; | ||||
|                 if (!length) { | ||||
|                     return e('span', null, `Fetching (${kB}kB)`); | ||||
|                 } | ||||
|                 const percent = Math.floor(100 * bytesReceived / length); | ||||
|                 return e('span', null, `Fetching (${kB}kB) ${percent}%`); | ||||
|             }, | ||||
|             success: () => e('span', null, "\u2713"), | ||||
|             error: (reason) => { | ||||
|                 return e('span', { className: 'error'}, `\u2717 ${reason}`); | ||||
|             }, | ||||
|         }, | ||||
|     )); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * The main component. | ||||
|  */ | ||||
| function BundlePicker() { | ||||
|     const [bundle, setBundle] = React.useState(""); | ||||
|     const [file, setFile] = React.useState(""); | ||||
|     const [line, setLine] = React.useState("1"); | ||||
|     const [column, setColumn] = React.useState(""); | ||||
|     const [result, setResult] = React.useState(None); | ||||
|     const [bundleFetchStatus, setBundleFetchStatus] = React.useState(None); | ||||
|     const [fileFetchStatus, setFileFetchStatus] = React.useState(None); | ||||
| 
 | ||||
|     /* At startup, try to fill in the bundle name for the user */ | ||||
|     React.useEffect(() => { | ||||
|         getBundleName().then((name) => { | ||||
|             if (bundle === "" && validateBundle(name) !== None) { | ||||
|                 setBundle(name); | ||||
|             } | ||||
|         }, console.log.bind(console)); | ||||
|     }, []); | ||||
| 
 | ||||
| 
 | ||||
|     /* ------------------------- */ | ||||
|     /* Follow user state changes */ | ||||
|     /* ------------------------- */ | ||||
|     const onBundleChange = React.useCallback((event) => { | ||||
|         const value = event.target.value; | ||||
|         setBundle(value); | ||||
|     }, []); | ||||
| 
 | ||||
|     const onFileChange = React.useCallback((event) => { | ||||
|         const value = event.target.value; | ||||
|         setFile(value); | ||||
|     }, []); | ||||
| 
 | ||||
|     const onLineChange = React.useCallback((event) => { | ||||
|         const value = event.target.value; | ||||
|         setLine(value); | ||||
|     }, []); | ||||
| 
 | ||||
|     const onColumnChange = React.useCallback((event) => { | ||||
|         const value = event.target.value; | ||||
|         setColumn(value); | ||||
|     }, []); | ||||
| 
 | ||||
| 
 | ||||
|     /* ------------------------------------------------ */ | ||||
|     /* Plumb data-fetching observables through to React */ | ||||
|     /* ------------------------------------------------ */ | ||||
| 
 | ||||
|     /* Whenever a valid bundle name is input, go see if it's a real bundle on the server */ | ||||
|     React.useEffect(() => | ||||
|         validateBundle(bundle).fold({ | ||||
|             some: (value) => { | ||||
|                 const subscription = bundleSubject(value) | ||||
|                     .pipe(rxjs.operators.map(Some.of)) | ||||
|                     .subscribe(setBundleFetchStatus); | ||||
|                 return () => subscription.unsubscribe(); | ||||
|             }, | ||||
|             none: () => setBundleFetchStatus(None), | ||||
|         }), | ||||
|     [bundle]); | ||||
| 
 | ||||
|     /* Whenever a valid javascript file is input, see if it corresponds to a sourcemap file and initiate a fetch | ||||
|      * if so. */ | ||||
|     React.useEffect(() => { | ||||
|         if (!file.match(/.\.js$/) || validateBundle(bundle) === None) { | ||||
|             setFileFetchStatus(None); | ||||
|             return; | ||||
|         } | ||||
|         const observable = fetchAsSubject(`/bundles/${bundle}/${file}.map`) | ||||
|             .pipe( | ||||
|                 rxjs.operators.map((fetchStatus) => fetchStatus.flatMap(value => { | ||||
|                     try { | ||||
|                         return Success.of(JSON.parse(value)); | ||||
|                     } catch (e) { | ||||
|                         return FetchError.of(e); | ||||
|                     } | ||||
|                 })), | ||||
|                 rxjs.operators.map(Some.of), | ||||
|             ); | ||||
|         const subscription = observable.subscribe(setFileFetchStatus); | ||||
|         return () => subscription.unsubscribe(); | ||||
|     }, [bundle, file]); | ||||
| 
 | ||||
|     /* | ||||
|      * Whenever we have a valid fetched sourcemap, and a valid line, attempt to find the original position from the | ||||
|      * sourcemap. | ||||
|      */ | ||||
|     React.useEffect(() => { | ||||
|         // `fold` dispatches on the datatype, like a switch statement
 | ||||
|         fileFetchStatus.fold({ | ||||
|             some: (fetchStatus) => | ||||
|                 // `fold` just returns null for all of the cases that aren't `Success` objects here
 | ||||
|                 fetchStatus.fold({ | ||||
|                     success: (value) => { | ||||
|                         if (!line) return setResult(None); | ||||
|                         const pLine = parseInt(line); | ||||
|                         const pCol = parseInt(column); | ||||
|                         sourceMap.SourceMapConsumer.with(value, undefined, (consumer) => | ||||
|                             consumer.originalPositionFor({ line: pLine, column: pCol }), | ||||
|                         ).then((result) => setResult(Some.of(JSON.stringify(result)))); | ||||
|                     }, | ||||
|                 }), | ||||
|             none: () => setResult(None), | ||||
|         }); | ||||
|     }, [fileFetchStatus, line, column]); | ||||
| 
 | ||||
| 
 | ||||
|     /* ------ */ | ||||
|     /* Render */ | ||||
|     /* ------ */ | ||||
|     return e('div', {}, | ||||
|         e('div', { className: 'inputs' }, | ||||
|             e('div', { className: 'bundle' }, | ||||
|                 e('label', { htmlFor: 'bundle'}, 'Bundle'), | ||||
|                 e('input', { | ||||
|                     name: 'bundle', | ||||
|                     required: true, | ||||
|                     pattern: "[0-9a-f]{20}", | ||||
|                     onChange: onBundleChange, | ||||
|                     value: bundle, | ||||
|                 }), | ||||
|                 bundleFetchStatus.fold({ | ||||
|                     some: (fetchStatus) => e(ProgressBar, { fetchStatus }), | ||||
|                     none: () => null, | ||||
|                 }), | ||||
|             ), | ||||
|             e('div', { className: 'file' }, | ||||
|                 e('label', { htmlFor: 'file' }, 'File'), | ||||
|                 e('input', { | ||||
|                     name: 'file', | ||||
|                     required: true, | ||||
|                     pattern: ".+\\.js", | ||||
|                     onChange: onFileChange, | ||||
|                     value: file, | ||||
|                 }), | ||||
|                 fileFetchStatus.fold({ | ||||
|                     some: (fetchStatus) => e(ProgressBar, { fetchStatus }), | ||||
|                     none: () => null, | ||||
|                 }), | ||||
|             ), | ||||
|             e('div', { className: 'line' }, | ||||
|                 e('label', { htmlFor: 'line' }, 'Line'), | ||||
|                 e('input', { | ||||
|                     name: 'line', | ||||
|                     required: true, | ||||
|                     pattern: "[0-9]+", | ||||
|                     onChange: onLineChange, | ||||
|                     value: line, | ||||
|                 }), | ||||
|             ), | ||||
|             e('div', { className: 'column' }, | ||||
|                 e('label', { htmlFor: 'column' }, 'Column'), | ||||
|                 e('input', { | ||||
|                     name: 'column', | ||||
|                     required: true, | ||||
|                     pattern: "[0-9]+", | ||||
|                     onChange: onColumnChange, | ||||
|                     value: column, | ||||
|                 }), | ||||
|             ), | ||||
|         ), | ||||
|         e('div', null, | ||||
|             result.fold({ | ||||
|                 none: () => "Select a bundle, file and line", | ||||
|                 some: (value) => e('pre', null, value), | ||||
|             }), | ||||
|         ), | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| /* Global stuff */ | ||||
| window.Decoder = { | ||||
|     BundlePicker, | ||||
| }; | ||||
|  | @ -0,0 +1,79 @@ | |||
| <html> | ||||
|     <head> | ||||
|         <title>Rageshake decoder ring</title> | ||||
|         <script crossorigin src="https://unpkg.com/source-map@0.7.3/dist/source-map.js"></script> | ||||
|         <script> | ||||
|             sourceMap.SourceMapConsumer.initialize({ | ||||
|                 "lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm" | ||||
|             }); | ||||
|         </script> | ||||
|         <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> | ||||
|         <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> | ||||
|         <!--<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> | ||||
|         <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>--> | ||||
|         <script crossorigin src="https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"></script> | ||||
|         <script src="datatypes.js"></script> | ||||
|         <script src="decoder.js"></script> | ||||
| 
 | ||||
|         <style> | ||||
|             @keyframes spin { | ||||
|                 from {transform:rotate(0deg);} | ||||
|                 to {transform:rotate(359deg);} | ||||
|             } | ||||
| 
 | ||||
|             body { | ||||
|                 font-family: sans-serif | ||||
|             } | ||||
| 
 | ||||
|             .spinner { | ||||
|                 animation: spin 4s infinite linear; | ||||
|                 display: inline-block; | ||||
|                 text-align: center; | ||||
|                 vertical-align: middle; | ||||
|                 font-size: larger; | ||||
|             } | ||||
| 
 | ||||
|             .progress { | ||||
|                 padding-left: 0.5em; | ||||
|                 padding-right: 0.5em; | ||||
|             } | ||||
| 
 | ||||
|             .bundle input { | ||||
|                 width: 24ex; | ||||
|             } | ||||
| 
 | ||||
|             .valid::after { | ||||
|                 content: "✓" | ||||
|             } | ||||
| 
 | ||||
|             label { | ||||
|                 width: 3em; | ||||
|                 margin-right: 1em; | ||||
|                 display: inline-block; | ||||
|             } | ||||
| 
 | ||||
|             input:valid { | ||||
|                 border: 1px solid green; | ||||
|             } | ||||
| 
 | ||||
|             .inputs > div { | ||||
|                 margin-bottom: 0.5em; | ||||
|             } | ||||
|         </style> | ||||
|     </head> | ||||
|     <body> | ||||
|         <header><h2>Decoder ring</h2></header> | ||||
|         <content id="main">Waiting for javascript to run...</content> | ||||
|         <script type="text/javascript"> | ||||
|             document.addEventListener("DOMContentLoaded", () => { | ||||
|                 try { | ||||
|                     ReactDOM.render(React.createElement(Decoder.BundlePicker), document.getElementById("main")) | ||||
|                 } catch (e) { | ||||
|                     const n = document.createElement("div"); | ||||
|                     n.innerText = `Error starting: ${e.message}`; | ||||
|                     document.getElementById("main").appendChild(n); | ||||
|                 } | ||||
|             }); | ||||
|         </script> | ||||
|     </body> | ||||
| </html> | ||||
|  | @ -63,11 +63,11 @@ const COPY_LIST = [ | |||
|     ["res/welcome/**", "webapp/welcome"], | ||||
|     ["res/themes/**", "webapp/themes"], | ||||
|     ["res/vector-icons/**", "webapp/vector-icons"], | ||||
|     ["res/decoder-ring/**", "webapp/decoder-ring"], | ||||
|     ["node_modules/matrix-react-sdk/res/media/**", "webapp/media"], | ||||
|     ["node_modules/olm/olm_legacy.js", "webapp", { directwatch: 1 }], | ||||
|     ["./config.json", "webapp", { directwatch: 1 }], | ||||
|     ["contribute.json", "webapp"], | ||||
|     ["node_modules/matrix-react-sdk/res/decoder-ring/**", "webapp/decoder-ring"], | ||||
| ]; | ||||
| 
 | ||||
| const parseArgs = require('minimist'); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Zoe
						Zoe