| @@ -0,0 +1,2 @@ | |||
| node_modules | |||
| public/bundle.* | |||
| @@ -0,0 +1,68 @@ | |||
| *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 | |||
| ``` | |||
| @@ -0,0 +1,25 @@ | |||
| { | |||
| "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" | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| html, body { | |||
| margin: 0px; | |||
| height: 100%; | |||
| } | |||
| * { | |||
| box-sizing: border-box; | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| <?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> | |||
| @@ -0,0 +1,17 @@ | |||
| <!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> | |||
| @@ -0,0 +1,39 @@ | |||
| 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() | |||
| ], | |||
| }; | |||
| @@ -0,0 +1,134 @@ | |||
| <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} | |||
| @@ -0,0 +1,123 @@ | |||
| <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> | |||
| @@ -0,0 +1,108 @@ | |||
| <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> | |||
| @@ -0,0 +1,88 @@ | |||
| <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> | |||
| @@ -0,0 +1,46 @@ | |||
| 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; | |||
| @@ -0,0 +1,83 @@ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,2 @@ | |||
| db.json | |||
| node_modules | |||
| @@ -0,0 +1,65 @@ | |||
| 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; | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| { | |||
| "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" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| { | |||
| "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" | |||
| } | |||
| } | |||
| @@ -0,0 +1,129 @@ | |||
| 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}.`); | |||