| node_modules | |||||
| public/bundle.* |
| *Psst — looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)* | |||||
| --- | |||||
| # svelte app | |||||
| This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. | |||||
| To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): | |||||
| ```bash | |||||
| npx degit sveltejs/template svelte-app | |||||
| cd svelte-app | |||||
| ``` | |||||
| *Note that you will need to have [Node.js](https://nodejs.org) installed.* | |||||
| ## Get started | |||||
| Install the dependencies... | |||||
| ```bash | |||||
| cd svelte-app | |||||
| npm install | |||||
| ``` | |||||
| ...then start [Rollup](https://rollupjs.org): | |||||
| ```bash | |||||
| npm run dev | |||||
| ``` | |||||
| Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. | |||||
| ## Deploying to the web | |||||
| ### With [now](https://zeit.co/now) | |||||
| Install `now` if you haven't already: | |||||
| ```bash | |||||
| npm install -g now | |||||
| ``` | |||||
| Then, from within your project folder: | |||||
| ```bash | |||||
| now | |||||
| ``` | |||||
| As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon. | |||||
| ### With [surge](https://surge.sh/) | |||||
| Install `surge` if you haven't already: | |||||
| ```bash | |||||
| npm install -g surge | |||||
| ``` | |||||
| Then, from within your project folder: | |||||
| ```bash | |||||
| npm run build | |||||
| surge public | |||||
| ``` |
| { | |||||
| "name": "svelte-app", | |||||
| "version": "1.0.0", | |||||
| "devDependencies": { | |||||
| "dev-refresh": "file:../../../../dev/dev-refresh", | |||||
| "npm-run-all": "^4.1.5", | |||||
| "rollup": "^1.10.1", | |||||
| "rollup-plugin-commonjs": "^9.3.4", | |||||
| "rollup-plugin-livereload": "^1.0.0", | |||||
| "rollup-plugin-node-resolve": "^4.2.3", | |||||
| "rollup-plugin-svelte": "^5.0.3", | |||||
| "rollup-plugin-terser": "^4.0.4", | |||||
| "svelte": "^3.0.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "sirv-cli": "^0.4.4" | |||||
| }, | |||||
| "scripts": { | |||||
| "build": "NODE_ENV=production rollup -c", | |||||
| "build:dev": "NODE_ENV=development rollup -c", | |||||
| "dev": "dev-refresh src --port 8080 --proxy 'http://localhost:8081' --cmd 'npm run build:dev' -n", | |||||
| "start": "sirv public --single", | |||||
| "start:dev": "sirv public --single --dev" | |||||
| } | |||||
| } |
| html, body { | |||||
| margin: 0px; | |||||
| height: 100%; | |||||
| } | |||||
| * { | |||||
| box-sizing: border-box; | |||||
| } |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||||
| <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |||||
| width="8px" height="8px" viewBox="0 0 8 8" enable-background="new 0 0 8 8" xml:space="preserve"> | |||||
| <rect x="-0.013" y="4.258" transform="matrix(-0.707 -0.7072 0.7072 -0.707 0.0891 10.1702)" width="4.33" height="1.618"/> | |||||
| <rect x="2.227" y="2.899" transform="matrix(-0.7071 0.7071 -0.7071 -0.7071 11.6877 2.6833)" width="6.121" height="1.726"/> | |||||
| </svg> |
| <!doctype html> | |||||
| <html> | |||||
| <head> | |||||
| <meta charset='utf8'> | |||||
| <meta name='viewport' content='width=device-width'> | |||||
| <title>Shopping List</title> | |||||
| <link rel='icon' type='image/png' href='/favicon.png'> | |||||
| <link rel='stylesheet' href='/global.css'> | |||||
| <link rel='stylesheet' href='/bundle.css'> | |||||
| </head> | |||||
| <body> | |||||
| <script src='/bundle.js'></script> | |||||
| </body> | |||||
| </html> |
| import svelte from 'rollup-plugin-svelte'; | |||||
| import resolve from 'rollup-plugin-node-resolve'; | |||||
| import commonjs from 'rollup-plugin-commonjs'; | |||||
| import { terser } from 'rollup-plugin-terser'; | |||||
| const production = !process.env.NODE_ENV || process.env.NODE_ENV == "production"; | |||||
| export default { | |||||
| input: 'src/main.js', | |||||
| output: { | |||||
| sourcemap: !production, | |||||
| format: 'iife', | |||||
| name: 'app', | |||||
| file: 'public/bundle.js' | |||||
| }, | |||||
| plugins: [ | |||||
| svelte({ | |||||
| // enable run-time checks when not in production | |||||
| dev: !production, | |||||
| // we'll extract any component CSS out into | |||||
| // a separate file — better for performance | |||||
| css: css => { | |||||
| css.write('public/bundle.css'); | |||||
| } | |||||
| }), | |||||
| // If you have external dependencies installed from | |||||
| // npm, you'll most likely need these plugins. In | |||||
| // some cases you'll need additional configuration — | |||||
| // consult the documentation for details: | |||||
| // https://github.com/rollup/rollup-plugin-commonjs | |||||
| resolve({ browser: true }), | |||||
| commonjs(), | |||||
| // If we're building for production (npm run build | |||||
| // instead of npm run dev), minify | |||||
| production && terser() | |||||
| ], | |||||
| }; |
| <script> | |||||
| import ListItem from './ListItem.svelte'; | |||||
| import ListInput from './ListInput.svelte'; | |||||
| import LoadingIcon from './LoadingIcon.svelte'; | |||||
| import { fade } from 'svelte/transition'; | |||||
| export let wsock; | |||||
| let words = {}; | |||||
| let items = {}; | |||||
| let pendingItems = []; | |||||
| let sortedItems = []; | |||||
| let connected = false; | |||||
| function sortItems() { | |||||
| let pending = []; | |||||
| for (let i in pendingItems) | |||||
| pending.push(pendingItems[i]); | |||||
| pending.sort((a, b) => b.index - a.index); | |||||
| let rest = []; | |||||
| for (let i in items) | |||||
| rest.push(items[i]); | |||||
| rest.sort((a, b) => b.index - a.index); | |||||
| sortedItems = []; | |||||
| for (let item of pending) | |||||
| sortedItems.push(item); | |||||
| for (let item of rest) | |||||
| sortedItems.push(item); | |||||
| } | |||||
| function onAdd(evt) { | |||||
| let content = evt.detail; | |||||
| let item = { pending: true, content, index: pendingItems.length }; | |||||
| pendingItems.push(item); | |||||
| sortItems(); | |||||
| wsock.send({ type: "item-add", content }).then(({ index }) => { | |||||
| delete pendingItems[item.index]; | |||||
| item.index = index; | |||||
| items[index] = item; | |||||
| if (words[content] == null) | |||||
| words[content] = 0; | |||||
| words[content] += 1; | |||||
| sortItems(); | |||||
| setTimeout(() => { item.pending = false; sortedItems = sortedItems; }, 50); | |||||
| }); | |||||
| } | |||||
| function onRemove(evt) { | |||||
| let item = evt.detail; | |||||
| if (item.pending) | |||||
| return; | |||||
| item.pending = true; | |||||
| sortedItems = sortedItems; | |||||
| wsock.send({ type: "item-del", index: item.index }).then(() => { | |||||
| delete items[item.index]; | |||||
| sortItems(); | |||||
| }); | |||||
| } | |||||
| export function onInitialData(data) { | |||||
| words = data.words; | |||||
| items = {}; | |||||
| for (let i in data.items) | |||||
| items[i] = { pending: false, content: data.items[i], index: i }; | |||||
| sortItems(); | |||||
| connected = true; | |||||
| } | |||||
| export function onDisconnect() { | |||||
| connected = false; | |||||
| } | |||||
| export function onAddFromServer(index, content) { | |||||
| let item = { pending: true, content, index }; | |||||
| items[index] = item; | |||||
| sortItems(); | |||||
| setTimeout(() => { item.pending = false; sortedItems = sortedItems; }, 10); | |||||
| } | |||||
| export function onRemoveFromServer(index) { | |||||
| delete items[index]; | |||||
| sortItems(); | |||||
| } | |||||
| </script> | |||||
| <style> | |||||
| .container { | |||||
| position: relative; | |||||
| margin: auto; | |||||
| text-align: center; | |||||
| width: 100%; | |||||
| max-width: 500px; | |||||
| height: 100%; | |||||
| } | |||||
| .items { | |||||
| margin-top: 24px; | |||||
| } | |||||
| .loading { | |||||
| display: flex; | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| position: absolute; | |||||
| top: 0px; | |||||
| left: 0px; | |||||
| justify-content: center; | |||||
| padding-top: 25vh; | |||||
| background: rgba(0, 0, 0, 0.7); | |||||
| } | |||||
| </style> | |||||
| <div class="container"> | |||||
| <ListInput {words} on:add={onAdd} /> | |||||
| <div class="items"> | |||||
| {#each sortedItems as item (item)} | |||||
| <ListItem {item} on:remove={onRemove} /> | |||||
| {/each} | |||||
| </div> | |||||
| </div> | |||||
| {#if !connected} | |||||
| <div out:fade={{duration: 100}} class="loading"><LoadingIcon /></div> | |||||
| {/if} |
| <script> | |||||
| import { createEventDispatcher } from 'svelte'; | |||||
| import { fade } from 'svelte/transition'; | |||||
| export let words; | |||||
| let dispatch = createEventDispatcher(); | |||||
| let inputEl; | |||||
| let hidden = true; | |||||
| let wordList = []; | |||||
| function updateWords() { | |||||
| wordList = []; | |||||
| for (let content in words) { | |||||
| if (content.includes(inputEl.value)) | |||||
| wordList.push({ content, score: words[content] }); | |||||
| } | |||||
| wordList.sort((a, b) => b.score - a.score); | |||||
| } | |||||
| function onBlur() { | |||||
| setTimeout(() => hidden = true, 50); | |||||
| } | |||||
| function onFocus() { | |||||
| updateWords(); | |||||
| hidden = false; | |||||
| } | |||||
| function onSubmit(evt) { | |||||
| let val = inputEl.value.toLowerCase(); | |||||
| if (!val) | |||||
| return; | |||||
| inputEl.value = ""; | |||||
| dispatch("add", val); | |||||
| } | |||||
| function add(content) { | |||||
| inputEl.value = ""; | |||||
| dispatch("add", content.toLowerCase()); | |||||
| } | |||||
| function capitalize(str) { | |||||
| var first = str[0].toUpperCase(); | |||||
| return first + str.substring(1); | |||||
| } | |||||
| </script> | |||||
| <style> | |||||
| .add { | |||||
| padding-top: 12px; | |||||
| } | |||||
| .add .content, | |||||
| .add .submit { | |||||
| box-sizing: content-box; | |||||
| -webkit-appearance: none; | |||||
| font-size: 16px; | |||||
| border: 1px solid #aaa; | |||||
| border-radius: 5px; | |||||
| background-color: #fff; | |||||
| height: 20px; | |||||
| padding: 6px; | |||||
| line-height: 0px; | |||||
| } | |||||
| .add .content { | |||||
| width: 78%; | |||||
| border-top-right-radius: 0px; | |||||
| border-bottom-right-radius: 0px; | |||||
| } | |||||
| .add .submit { | |||||
| width: 8%; | |||||
| background-color: #eee; | |||||
| border-top-left-radius: 0px; | |||||
| border-bottom-left-radius: 0px; | |||||
| } | |||||
| .add .submit:hover { | |||||
| background-color: #ddd; | |||||
| } | |||||
| .add .suggestions { | |||||
| z-index: 2; | |||||
| background: white; | |||||
| box-shadow: 0px 0px 10px 1px #ccc; | |||||
| position: absolute; | |||||
| width: 90%; | |||||
| margin: auto; | |||||
| left: 0px; | |||||
| right: 0px; | |||||
| margin-top: 15px; | |||||
| padding-top: 6px; | |||||
| padding-bottom: 6px; | |||||
| max-height: 70%; | |||||
| overflow-y: auto; | |||||
| overflow-x: hidden; | |||||
| } | |||||
| .add .suggestions .suggestion:last-child { | |||||
| border-bottom: none; | |||||
| } | |||||
| .add .suggestions .suggestion { | |||||
| padding: 12px; | |||||
| cursor: pointer; | |||||
| border-bottom: 1px solid #ccc; | |||||
| text-overflow: ellipsis; | |||||
| max-width: 100%; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <form on:submit|preventDefault={onSubmit} class="add"> | |||||
| <input | |||||
| class="content" name="content" autocomplete="off" | |||||
| on:focus={onFocus} on:blur={onBlur} on:keydown={() => window.setTimeout(updateWords, 10)} bind:this={inputEl}> | |||||
| <button class="submit" type="submit">+</button> | |||||
| {#if !hidden} | |||||
| <div transition:fade={{duration: 50}} class="suggestions"> | |||||
| {#each wordList as word} | |||||
| <div class="suggestion" on:click={() => add(word.content)}> | |||||
| {capitalize(word.content)} | |||||
| </div> | |||||
| {/each} | |||||
| </div> | |||||
| {/if} | |||||
| </form> |
| <script> | |||||
| import { createEventDispatcher } from 'svelte'; | |||||
| export let item; | |||||
| let dispatch = createEventDispatcher(); | |||||
| let emitRemove = () => dispatch("remove", item); | |||||
| function transIn(node) { | |||||
| if (!item.pending) return { delay: 0, duration: 0 }; | |||||
| let comp = getComputedStyle(node); | |||||
| let height = parseInt(comp.height); | |||||
| let paddingTop = parseInt(comp["padding-top"]); | |||||
| return { | |||||
| delay: 0, | |||||
| duration: 200, | |||||
| css: t => ` | |||||
| overflow: hidden; | |||||
| height: ${t * height}px; | |||||
| padding-top: ${t * paddingTop}px;` | |||||
| }; | |||||
| } | |||||
| function transOut(node) { | |||||
| let comp = getComputedStyle(node); | |||||
| let opacity = parseFloat(comp.opacity); | |||||
| let height = parseInt(comp.height); | |||||
| let paddingTop = parseInt(comp["padding-top"]); | |||||
| function part(t, from, to) { | |||||
| return Math.max((t - from), 0) / (to - from); | |||||
| } | |||||
| return { | |||||
| delay: 0, | |||||
| duration: 200, | |||||
| css: t => ` | |||||
| overflow: hidden; | |||||
| height: ${part(t, 0.3, 1) * height}px; | |||||
| padding-top: ${part(t, 0.3, 1) * paddingTop}px; | |||||
| transform: translateX(-${(1 - t) * 150}px); | |||||
| opacity: ${part(t, 0, 0.7) * opacity}` | |||||
| }; | |||||
| } | |||||
| function capitalize(str) { | |||||
| var first = str[0].toUpperCase(); | |||||
| return first + str.substring(1); | |||||
| } | |||||
| </script> | |||||
| <style> | |||||
| .item { | |||||
| transition: background-color 0.3s; | |||||
| padding-left: 12px; | |||||
| padding-top: 42px; | |||||
| position: relative; | |||||
| margin: auto; | |||||
| text-align: left; | |||||
| width: 100%; | |||||
| height: 70px; | |||||
| overflow: hidden; | |||||
| border-bottom: 1px solid #ccc; | |||||
| } | |||||
| .item.pending { | |||||
| background-color: #eee; | |||||
| } | |||||
| .item .name { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| max-width: calc(100% - 60px); | |||||
| overflow: hidden; | |||||
| text-overflow: ellipsis; | |||||
| } | |||||
| .item .ok { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| float: right; | |||||
| margin-right: 6px; | |||||
| height: 50px; | |||||
| width: 50px; | |||||
| bottom: 33px; | |||||
| background-image: url(/icon-check.svg); | |||||
| background-size: 24px 24px; | |||||
| background-repeat: no-repeat; | |||||
| background-position: center; | |||||
| cursor: pointer; | |||||
| border-radius: 100px; | |||||
| background-color: #eee; | |||||
| } | |||||
| .item .ok:hover { | |||||
| background-color: #ddd; | |||||
| } | |||||
| </style> | |||||
| <div in:transIn out:transOut class={`item ${item.pending ? 'pending' : ''}`}> | |||||
| <div class="name">{capitalize(item.content)}</div> | |||||
| {#if !item.pending} | |||||
| <span class="ok" on:click={emitRemove}></span> | |||||
| {/if} | |||||
| </div> |
| <style> | |||||
| .lds-default { | |||||
| display: inline-block; | |||||
| position: relative; | |||||
| width: 64px; | |||||
| height: 64px; | |||||
| } | |||||
| .lds-default div { | |||||
| position: absolute; | |||||
| width: 5px; | |||||
| height: 5px; | |||||
| background: #fff; | |||||
| border-radius: 50%; | |||||
| animation: lds-default 1.2s linear infinite; | |||||
| } | |||||
| .lds-default div:nth-child(1) { | |||||
| animation-delay: 0s; | |||||
| top: 29px; | |||||
| left: 53px; | |||||
| } | |||||
| .lds-default div:nth-child(2) { | |||||
| animation-delay: -0.1s; | |||||
| top: 18px; | |||||
| left: 50px; | |||||
| } | |||||
| .lds-default div:nth-child(3) { | |||||
| animation-delay: -0.2s; | |||||
| top: 9px; | |||||
| left: 41px; | |||||
| } | |||||
| .lds-default div:nth-child(4) { | |||||
| animation-delay: -0.3s; | |||||
| top: 6px; | |||||
| left: 29px; | |||||
| } | |||||
| .lds-default div:nth-child(5) { | |||||
| animation-delay: -0.4s; | |||||
| top: 9px; | |||||
| left: 18px; | |||||
| } | |||||
| .lds-default div:nth-child(6) { | |||||
| animation-delay: -0.5s; | |||||
| top: 18px; | |||||
| left: 9px; | |||||
| } | |||||
| .lds-default div:nth-child(7) { | |||||
| animation-delay: -0.6s; | |||||
| top: 29px; | |||||
| left: 6px; | |||||
| } | |||||
| .lds-default div:nth-child(8) { | |||||
| animation-delay: -0.7s; | |||||
| top: 41px; | |||||
| left: 9px; | |||||
| } | |||||
| .lds-default div:nth-child(9) { | |||||
| animation-delay: -0.8s; | |||||
| top: 50px; | |||||
| left: 18px; | |||||
| } | |||||
| .lds-default div:nth-child(10) { | |||||
| animation-delay: -0.9s; | |||||
| top: 53px; | |||||
| left: 29px; | |||||
| } | |||||
| .lds-default div:nth-child(11) { | |||||
| animation-delay: -1s; | |||||
| top: 50px; | |||||
| left: 41px; | |||||
| } | |||||
| .lds-default div:nth-child(12) { | |||||
| animation-delay: -1.1s; | |||||
| top: 41px; | |||||
| left: 50px; | |||||
| } | |||||
| @keyframes lds-default { | |||||
| 0%, 20%, 80%, 100% { | |||||
| transform: scale(1); | |||||
| } | |||||
| 50% { | |||||
| transform: scale(1.5); | |||||
| } | |||||
| } | |||||
| </style> | |||||
| <div class="lds-default"> | |||||
| <div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div> | |||||
| </div> |
| import App from './App.svelte'; | |||||
| import WSockMan from './ws.js'; | |||||
| let wsock = new WSockMan( | |||||
| `${location.protocol == "http:" ? "ws:" : "wss:"}//${location.host}/`); | |||||
| let app = new App({ target: document.body, props: { wsock }}); | |||||
| let key = localStorage.getItem("key"); | |||||
| if (!key) | |||||
| key = prompt("Key?"); | |||||
| function auth(k) { | |||||
| wsock.send({ type: "init", key: k }) | |||||
| .then(res => { | |||||
| localStorage.setItem("key", key); | |||||
| key = k; | |||||
| app.onInitialData(res.data); | |||||
| }) | |||||
| .catch(err => { | |||||
| console.trace(err); | |||||
| let k = prompt(err); | |||||
| if (k != null) | |||||
| auth(k); | |||||
| }); | |||||
| }; | |||||
| wsock.onconnect = () => auth(key); | |||||
| wsock.ondisconnect = app.onDisconnect.bind(app); | |||||
| wsock.onmessage = msg => { | |||||
| switch (msg.type) { | |||||
| case "item-del": | |||||
| app.onRemoveFromServer(msg.index); | |||||
| break; | |||||
| case "item-add": | |||||
| app.onAddFromServer(msg.index, msg.content); | |||||
| break; | |||||
| default: | |||||
| console.warn("Unknown message type", msg.type); | |||||
| } | |||||
| }; | |||||
| export default app; |
| export default class WSockMan { | |||||
| constructor(url) { | |||||
| this.ready = false; | |||||
| this.sock = null; | |||||
| this.url = url; | |||||
| this.sendQ = []; | |||||
| this.ackQ = []; | |||||
| this.ackID = 0; | |||||
| this.ondisconnect = () => {}; | |||||
| this.onconnect = () => {}; | |||||
| this.onmessage = () => {}; | |||||
| this.createWS(); | |||||
| } | |||||
| send(obj, cb) { | |||||
| return new Promise((resolve, reject) => { | |||||
| if (this.ready) { | |||||
| return this.reallySend(obj, resolve, reject); | |||||
| } else { | |||||
| this.sendQ.push([obj, resolve, reject]); | |||||
| } | |||||
| }); | |||||
| } | |||||
| createWS() { | |||||
| if (this.sock) | |||||
| this.sock.close(); | |||||
| this.sock = new WebSocket(this.url); | |||||
| this.sock.onopen = () => { | |||||
| this.ready = true; | |||||
| this.ackID = 0; | |||||
| for (let i = 0; i < this.sendQ.length; ++i) | |||||
| this.reallySend(...this.sendQ[i]); | |||||
| this.sendQ = []; | |||||
| this.onconnect(); | |||||
| }; | |||||
| this.sock.onclose = () => { | |||||
| console.error("Connection closed."); | |||||
| this.ready = false; | |||||
| this.sock = null; | |||||
| for (let i = 0; i < this.ackQ.length; ++i) { | |||||
| let [ resolve, reject ] = this.ackQ[i]; | |||||
| reject("Lost connection."); | |||||
| } | |||||
| this.ackQ = []; | |||||
| this.ondisconnect(); | |||||
| setTimeout(() => this.createWS(), 1000); | |||||
| }; | |||||
| this.sock.onerror = evt => { | |||||
| console.error("WebSocket error:", evt); | |||||
| }; | |||||
| this.sock.onmessage = evt => { | |||||
| let obj = JSON.parse(evt.data); | |||||
| if (obj.type == "ack") { | |||||
| let [ resolve, reject ] = this.ackQ[obj.ackID]; | |||||
| if (obj.error) | |||||
| reject(obj.error); | |||||
| else | |||||
| resolve(obj); | |||||
| } else { | |||||
| this.onmessage(obj); | |||||
| } | |||||
| }; | |||||
| } | |||||
| reallySend(obj, resolve, reject) { | |||||
| let ackID = this.ackID++; | |||||
| this.ackQ[ackID] = [ resolve, reject ]; | |||||
| obj.ackID = ackID; | |||||
| this.sock.send(JSON.stringify(obj)); | |||||
| } | |||||
| } |
| db.json | |||||
| node_modules |
| let http = require("http"); | |||||
| let pathlib = require("path"); | |||||
| let fs = require("fs"); | |||||
| module.exports = function createFileServer(webroot) { | |||||
| let server = http.createServer((req, res) => { | |||||
| console.log(req.method+" "+req.url); | |||||
| if (req.method != "HEAD" && req.method != "GET") { | |||||
| req.writeHead(405); | |||||
| req.end(`Unexpected method: ${req.method}`); | |||||
| return; | |||||
| } | |||||
| let path = pathlib.normalize(pathlib.join(webroot, req.url)); | |||||
| if (!path.startsWith(webroot)) { | |||||
| res.writeHead(404); | |||||
| res.end(`404 Not Found: ${req.url}`); | |||||
| return; | |||||
| } | |||||
| (function read(path) { | |||||
| let rs = fs.createReadStream(path); | |||||
| rs.once("error", err => { | |||||
| if (err.code == "ENOENT") { | |||||
| res.writeHead(404); | |||||
| res.end(`404 Not Found: ${req.url}`); | |||||
| } else if (err.code == "EISDIR") { | |||||
| if (!req.url.endsWith("/")) { | |||||
| res.writeHead(302, { | |||||
| location: `${req.url}/`, | |||||
| }); | |||||
| res.end(`302 Found: ${req.url}/`); | |||||
| } else { | |||||
| read(path+"/index.html"); | |||||
| } | |||||
| } else { | |||||
| console.warn(`Failed to open ${path}`); | |||||
| console.trace(err); | |||||
| res.writeHead(500); | |||||
| res.end("500 Internal Server Error"); | |||||
| } | |||||
| }); | |||||
| rs.once("open", () => { | |||||
| let ctype = null; | |||||
| if (path.endsWith(".html")) | |||||
| ctype = "text/html"; | |||||
| else if (path.endsWith(".js")) | |||||
| ctype = "application/javascript"; | |||||
| else if (path.endsWith(".css")) | |||||
| ctype = "text/css"; | |||||
| if (ctype) | |||||
| res.writeHead(200, { "Content-Type": ctype }); | |||||
| else | |||||
| res.writeHead(200); | |||||
| rs.pipe(res); | |||||
| }); | |||||
| })(path); | |||||
| }); | |||||
| return server; | |||||
| } |
| { | |||||
| "name": "server", | |||||
| "version": "1.0.0", | |||||
| "lockfileVersion": 1, | |||||
| "requires": true, | |||||
| "dependencies": { | |||||
| "async-limiter": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", | |||||
| "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" | |||||
| }, | |||||
| "dev-refresh": { | |||||
| "version": "file:../../../../dev/dev-refresh", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "colors": "^1.3.3", | |||||
| "minimist": "^1.2.0", | |||||
| "node-watch": "^0.6.2", | |||||
| "open": "^6.4.0", | |||||
| "webframe": "^0.9.0", | |||||
| "ws": "^7.1.0" | |||||
| } | |||||
| }, | |||||
| "ws": { | |||||
| "version": "7.0.1", | |||||
| "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.1.tgz", | |||||
| "integrity": "sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A==", | |||||
| "requires": { | |||||
| "async-limiter": "^1.0.0" | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| { | |||||
| "name": "server", | |||||
| "version": "1.0.0", | |||||
| "description": "", | |||||
| "main": "index.js", | |||||
| "scripts": { | |||||
| "start": "node server.js'", | |||||
| "dev": "dev-refresh *.js --cmd 'KEY=test PORT=8081 node server.js'" | |||||
| }, | |||||
| "author": "", | |||||
| "license": "ISC", | |||||
| "dependencies": { | |||||
| "ws": "^7.0.1" | |||||
| }, | |||||
| "devDependencies": { | |||||
| "dev-refresh": "file:../../../../dev/dev-refresh" | |||||
| } | |||||
| } |
| let WebSocket = require("ws"); | |||||
| let createFileServer = require("./filesrv"); | |||||
| let fs = require("fs"); | |||||
| let datafile = "db.json"; | |||||
| let scratchfile = ".db.scratch"; | |||||
| let key = process.env.KEY; | |||||
| if (!key) { | |||||
| console.log("Environmnt variable 'KEY' not set."); | |||||
| process.exit(1); | |||||
| } | |||||
| let server = createFileServer("../client/public"); | |||||
| let wss = new WebSocket.Server({ server }); | |||||
| function deserialize(defaults) { | |||||
| try { | |||||
| let json = fs.readFileSync(datafile); | |||||
| return JSON.parse(json); | |||||
| } catch (err) { | |||||
| return defaults; | |||||
| } | |||||
| } | |||||
| function serialize(data) { | |||||
| fs.writeFileSync(scratchfile, JSON.stringify(data)); | |||||
| fs.renameSync(scratchfile, datafile); | |||||
| } | |||||
| let data = deserialize({ | |||||
| items: { "0": "foo", "2": "bar" }, | |||||
| words: {}, | |||||
| nextIdx: 3, | |||||
| }); | |||||
| let conns = []; | |||||
| let connID = 0; | |||||
| function ack(conn, msg, reply = {}) { | |||||
| reply.type = "ack"; | |||||
| reply.ackID = msg.ackID; | |||||
| conn.sock.send(JSON.stringify(reply)); | |||||
| } | |||||
| function broadcast(from, obj) { | |||||
| for (let conn of conns) { | |||||
| if (!conn) continue; | |||||
| if (conn != from) | |||||
| conn.sock.send(JSON.stringify(obj)); | |||||
| } | |||||
| } | |||||
| function onMessage(conn, msg) { | |||||
| if (msg.type == "init") { | |||||
| if (!msg.key || msg.key != key) | |||||
| return ack(conn, msg, { error: "Invalid key." }); | |||||
| ack(conn, msg, { data }); | |||||
| conn.ready = true; | |||||
| conns[conn.id] = conn; | |||||
| return; | |||||
| } | |||||
| if (!conn.ready) | |||||
| return ack(conn, msg, { error: "Not initialized." }); | |||||
| switch (msg.type) { | |||||
| case "item-add": | |||||
| if (typeof msg.content != "string") | |||||
| return ack(conn, msg, { error: "Missing content." }); | |||||
| let content = msg.content.toLowerCase(); | |||||
| let index = data.nextIdx.toString(); | |||||
| data.nextIdx = (data.nextIdx + 1) % 2048; | |||||
| data.items[index] = content; | |||||
| if (data.words[content] == null) | |||||
| data.words[content] = 0; | |||||
| data.words[content] += 1; | |||||
| ack(conn, msg, { index }); | |||||
| broadcast(conn, { type: "item-add", content, index }); | |||||
| serialize(data); | |||||
| break; | |||||
| case "item-del": | |||||
| if (typeof msg.index != "string") | |||||
| return ack(conn, msg, { error: "Missing index." }); | |||||
| delete data.items[msg.index]; | |||||
| ack(conn, msg); | |||||
| broadcast(conn, { type: "item-del", index: msg.index }); | |||||
| serialize(data); | |||||
| break; | |||||
| } | |||||
| } | |||||
| wss.on("connection", sock => { | |||||
| let conn = { | |||||
| sock, | |||||
| ready: false, | |||||
| id: connID, | |||||
| }; | |||||
| connID = (connID + 1) % 2048; | |||||
| sock.on("close", () => { | |||||
| delete conns[conn.id]; | |||||
| }); | |||||
| sock.on("message", msg => { | |||||
| let obj; | |||||
| try { | |||||
| obj = JSON.parse(msg); | |||||
| } catch (err) { | |||||
| console.error("Client message has invalid JSON:", err); | |||||
| console.error(err); | |||||
| return; | |||||
| } | |||||
| onMessage(conn, obj); | |||||
| }); | |||||
| }); | |||||
| let port = process.env.PORT ? parseInt(process.env.PORT) : 8080; | |||||
| server.listen(port); | |||||
| console.log(`Listening on port ${port}.`); |