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}.`); |