diff --git a/gun-with-cancel.js b/gun-with-cancel.js new file mode 100644 index 0000000..9b6c5ec --- /dev/null +++ b/gun-with-cancel.js @@ -0,0 +1,26 @@ +module.exports = function patchGunWithCancel(Gun) { + Gun.chain.onWithCancel = (function() { + function cancelCallback(data,key,msg,ev) { + if (ev && typeof ev.off === 'function') { + ev.off(); + } + } + + return function(tag, arg, eas, as) { + if (typeof tag === 'function') { + let callback = tag; + const cancelEv = function() { + callback = cancelCallback; + }; + const wrapper = function() { + return callback.apply(this, arguments); + }; + this.on(wrapper, arg, eas, as); + return cancelEv; + } else { + this.on(tag, arg, eas, as); + return null; + } + }; + })(); +}; diff --git a/index.js b/index.js index 9c0e4bd..6a34739 100644 --- a/index.js +++ b/index.js @@ -1,45 +1,34 @@ -var config = { port: process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 8765 }; +'use strict'; var fs = require('fs'); var Gun = require('gun'); var SEA = require('gun/sea'); var NTS = require('gun/nts'); var rtc = require('gun/lib/webrtc'); - var sqlite3 = require('./sqlite3-promises'); +var express = require('express'); -if(process.env.HTTPS_KEY){ +require('./gun-with-cancel')(Gun); + +const POLLING_TIMEOUT = 60000/*ms*/; + +const app = express(); + +var config = { port: process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 8765 }; + +if (process.env.HTTPS_KEY) { config.key = fs.readFileSync(process.env.HTTPS_KEY); config.cert = fs.readFileSync(process.env.HTTPS_CERT); - config.server = require('https').createServer(config, Gun.serve(__dirname)); + config.server = require('https').createServer(config, app); } else { - config.server = require('http').createServer(Gun.serve(__dirname)); + config.server = require('http').createServer(app); } -Gun.chain.onWithCancel = (function() { - function cancelCallback(data,key,msg,ev) { - if (ev && typeof ev.off === 'function') { - ev.off(); - } - } - - return function(tag, arg, eas, as) { - if (typeof tag === 'function') { - let callback = tag; - const cancelEv = function() { - callback = cancelCallback; - }; - const wrapper = function() { - return callback.apply(this, arguments); - }; - this.on(wrapper, arg, eas, as); - return cancelEv; - } else { - this.on(tag, arg, eas, as); - return null; - } - }; -})(); +var gun = Gun({ + web: config.server, + peers: ['https://jessemcdonald.info/gun'], +}); +console.log('Relay peer started on port ' + config.port + ' with /gun'); var appendJournal = (function() { let lastJournalText = null; @@ -51,12 +40,6 @@ var appendJournal = (function() { }; })(); -var gun = Gun({ - web: config.server.listen(config.port), - peers: ['https://jessemcdonald.info/gun'], -}); -console.log('Relay peer started on port ' + config.port + ' with /gun'); - function logIn(msg){ console.log(`in msg:${JSON.stringify(msg)}.........`); } @@ -88,72 +71,181 @@ function logDbError(label) { const dbInit = (async function dbInit() { const db = await sqlite3.openAsync('./pacosako.db'); - await db.runAsync(` - CREATE TABLE IF NOT EXISTS games ( - gameId TEXT PRIMARY KEY, - board TEXT - ) - `); + try { + await db.runAsync(`VACUUM`); + } catch(err) { + logDbError('vacuum')(err); + } await db.runAsync(` - CREATE TABLE IF NOT EXISTS meta ( + CREATE TABLE IF NOT EXISTS games ( gameId TEXT PRIMARY KEY, lightName TEXT NOT NULL, darkName TEXT NOT NULL, moves INTEGER NOT NULL, - status TEXT, - timestamp INTEGER NOT NULL + status TEXT NOT NULL, + timestamp INTEGER NOT NULL, + board TEXT NOT NULL, + added INTEGER NOT NULL, + modified INTEGER NOT NULL ) `); + await db.runAsync(` + CREATE INDEX IF NOT EXISTS games_timestamp ON games(timestamp) + `); + + await db.runAsync(` + CREATE INDEX IF NOT EXISTS games_modified ON games(modified) + `); + console.log('Connected to the SQLite database.'); return db; })().catch(logDbError('dbInit')); +function pruneEmpty(obj) { + const copy = {}; + for (const key in obj) { + const val = obj[key]; + if (val !== undefined && val !== null && val !== '') { + copy[key] = obj[key]; + } + } + return copy; +} + async function logAllMeta() { const db = await dbInit; try { - for await (const row of db.eachAsync(`SELECT * FROM meta ORDER BY timestamp DESC`)) { - console.log(JSON.stringify(row)); + const querySql = ` + SELECT gameId, lightName, darkName, moves, status, timestamp FROM games ORDER BY timestamp DESC + `; + for await (const row of db.eachAsync(querySql)) { + console.log(JSON.stringify(pruneEmpty(row))); } } catch(err) { logDbError('logAllMeta')(err); } } +let waitingAnyGame = null; +const waitingGames = {}; + +function signalGameUpdate(gameId) { + const promise = waitingGames[gameId]; + if (promise) { + delete waitingGames[gameId]; + promise.resolve(); + } + const anyPromise = waitingAnyGame; + if (anyPromise) { + waitingAnyGame = null; + anyPromise.resolve(); + } +} + +function waitForGameUpdate(gameId) { + let promise = waitingGames[gameId]; + if (promise === undefined) { + let resolve; + promise = waitingGames[gameId] = new Promise((res) => { resolve = res; }); + promise.resolve = resolve; + } + return promise; +} + +function waitForAnyGameUpdate() { + if (waitingAnyGame === null) { + let resolve; + waitingAnyGame = new Promise((res) => { resolve = res; }); + waitingAnyGame.resolve = resolve; + } + return waitingAnyGame; +} + +function waitFor(duration) { + return new Promise((resolve, reject) => setTimeout(() => { resolve(); }, duration)); +} + //logAllMeta(); -async function updateMeta(gameId, meta) { +function checkString(value, label, dflt) { + try { + if (arguments.length >= 3 && (value === undefined || value === null)) { + return dflt; + } else if (typeof value === 'string') { + return value; + } + } catch (err) {} + throw { message: `${label || 'value'} should be a string` }; +} + +function checkInteger(value, label, dflt) { + try { + if (arguments.length >= 3 && (value === undefined || value === null)) { + return dflt; + } else if (Number.isInteger(value)) { + return value; + } + } catch(err) {} + throw { message: `${label || 'value'} should be an integer` }; +} + +async function updateMeta(gameId, meta, time) { + if (arguments.length < 3) { + time = +new Date(); + } + const db = await dbInit; - const insertSql = - `INSERT OR REPLACE INTO meta - ( gameId, lightName, darkName, moves, status, timestamp) - VALUES - ($gameId, $lightName, $darkName, $moves, $status, $timestamp)`; + const insertSql = ` + INSERT INTO games (gameId, lightName, darkName, moves, status, timestamp, board, added, modified) + VALUES ($gameId, $lightName, $darkName, $moves, $status, $timestamp, '', $time, $time) + ON CONFLICT (gameId) DO UPDATE + SET lightName = $lightName, darkName = $darkName, moves = $moves, + status = $status, timestamp = $timestamp, modified = $time + WHERE (lightName <> $lightName OR darkName <> $darkName OR moves <> $moves OR + status <> $status OR timestamp <> $timestamp) + AND modified < $time + `; await db.runAsync(insertSql, { $gameId: gameId, - $lightName: String(meta.lightName || ''), - $darkName: String(meta.darkName || ''), - $moves: Number(meta.moves) || 0, - $status: (meta.status ? String(meta.status) : null), - $timestamp: Number(meta.timestamp) || 0, + $lightName: checkString(meta.lightName, 'meta.lightName', ''), + $darkName: checkString(meta.darkName, 'meta.darkName', ''), + $moves: checkInteger(meta.moves, 'meta.moves', 0), + $status: checkString(meta.status, 'meta.status', ''), + $timestamp: checkInteger(meta.timestamp, 'meta.timestamp', 0), + $time: time, }); + signalGameUpdate(gameId); + return db; } -async function updateGame(gameId, moves) { +async function updateGame(gameId, moves, time) { + if (arguments.length < 3) { + time = +new Date(); + } + const db = await dbInit; - const insertSql = - `INSERT OR REPLACE INTO games (gameId, board) VALUES ($gameId, $board)`; + const insertSql = ` + INSERT INTO games (gameId, lightName, darkName, moves, status, timestamp, board, added, modified) + VALUES ($gameId, '', '', 0, '', 0, '', $time, $time) + ON CONFLICT (gameId) DO UPDATE + SET board = $board, modified = $time + WHERE (board <> $board) AND modified < $time + `; await db.runAsync(insertSql, { $gameId: gameId, $board: JSON.stringify(moves), + $time: time, }); + signalGameUpdate(gameId); + return db; } @@ -211,3 +303,185 @@ gun.get(PacoSakoUUID + '/meta').on(function(meta) { } } }, { change: true }); + +function internalErrorJson(err, res) { + res.status(500); + if (err && 'message' in err) { + console.error(err.message); + res.json({ message: 'internal error: ' + err.message }); + } else { + console.error(err); + res.json({ message: 'internal error' }); + } +} + +async function getGameListHandler(req, res, next) { + res.set('Cache-Control', 'no-store'); + try { + const afterTime = req.params.afterTime; + + if (afterTime !== undefined && !afterTime.match(/^\d+$/)) { + res.status(400).json({ message: 'malformed time' }); + return; + } + + const pollTimeout = waitFor(POLLING_TIMEOUT).then(() => 'timeout').catch(()=>{}); + + while (true) { + /* Save the async promise _before_ the query so we don't miss any updates while suspended. */ + const gameUpdate = waitForAnyGameUpdate().then(() => 'update'); + + const cutoff = (+new Date()) - (2 * 7 * 24 * 3600 * 1000); /* 2 weeks ago */ + const whereClause = (afterTime === undefined) ? `true` : `modified > $afterTime`; + const querySql = ` + SELECT gameId, lightName, darkName, moves, status, timestamp, modified + FROM games WHERE timestamp >= $cutoff AND ${whereClause} ORDER BY timestamp DESC + LIMIT 1000 + `; + const results = await (await dbInit).allAsync(querySql, { + $afterTime: checkInteger(Number(afterTime), 'afterTime', 0), + $cutoff: cutoff, + }); + + if (afterTime === undefined || results.length > 0) { + let lastModified = afterTime || 0; + for (const result of results) { + if (result.modified > lastModified) { + lastModified = result.modified; + } + } + + res.json({ games: results.map(pruneEmpty), modified: lastModified }); + return; + } + + if (await Promise.race([gameUpdate, pollTimeout]) === 'timeout') { + res.status(204).json({ retry: true }); + return; + } + } + } catch (err) { + internalErrorJson(err, res); + } +} + +async function getGameHandler(req, res, next) { + res.set('Cache-Control', 'no-store'); + try { + const gameId = req.params.gameId; + const afterTime = req.params.afterTime; + + if (!gameId.match(/^[0-9a-f]{16}$/)) { + res.status(400).json({ message: 'malformed game ID' }); + return; + } + + if (afterTime !== undefined && !afterTime.match(/^\d+$/)) { + res.status(400).json({ message: 'malformed time' }); + return; + } + + const pollTimeout = waitFor(POLLING_TIMEOUT).then(() => 'timeout').catch(()=>{}); + + while (true) { + /* Save the async promise _before_ the query so we don't miss any updates while suspended. */ + const gameUpdate = waitForGameUpdate(gameId).then(() => 'update'); + + const querySql = ` + SELECT board, modified FROM games WHERE gameId = $gameId + `; + const result = await (await dbInit).getAsync(querySql, { $gameId: gameId }); + + if (!result) { + res.status(404).json({ message: 'unknown game ID' }); + return; + } + + if (afterTime === undefined || result.modified > afterTime) { + const parsed = (result.board === '') ? null : JSON.parse(result.board); + res.json({ board: parsed, modified: result.modified }); + return; + } + + if (await Promise.race([gameUpdate, pollTimeout]) === 'timeout') { + res.status(204).json({ retry: true }); + return; + } + } + } catch(err) { + internalErrorJson(err, res); + } +} + +async function getMetaHandler(req, res, next) { + res.set('Cache-Control', 'no-store'); + try { + const gameId = req.params.gameId; + const afterTime = req.params.afterTime; + + if (!gameId.match(/^[0-9a-f]{16}$/)) { + res.status(400).json({ message: 'malformed game ID' }); + return; + } + + if (afterTime !== undefined && !afterTime.match(/^\d+$/)) { + res.status(400).json({ message: 'malformed time' }); + return; + } + + const pollTimeout = waitFor(POLLING_TIMEOUT).then(() => 'timeout').catch(()=>{}); + + while (true) { + /* Save the async promise _before_ the query so we don't miss any updates while suspended. */ + const metaUpdate = waitForGameUpdate(gameId).then(() => 'update'); + + const querySql = ` + SELECT lightName, darkName, moves, status, timestamp, modified FROM games + WHERE gameId = $gameId + `; + const result = await (await dbInit).getAsync(querySql, { $gameId: gameId }); + + if (!result) { + res.status(404).json({ message: 'unknown game ID' }); + return; + } + + if (afterTime === undefined || result.modified > afterTime) { + res.json(pruneEmpty(result)); + return; + } + + if (await Promise.race([metaUpdate, pollTimeout]) === 'timeout') { + res.status(204).json({ retry: true }); + return; + } + } + } catch (err) { + internalErrorJson(err, res); + } +} + +app.get('/pacosako/api/games/poll/:afterTime', getGameListHandler); +app.get('/pacosako/api/games', getGameListHandler); + +app.get('/pacosako/api/game/:gameId/poll/:afterTime', getGameHandler); +app.get('/pacosako/api/game/:gameId', getGameHandler); + +app.get('/pacosako/api/meta/:gameId/poll/:afterTime', getMetaHandler); +app.get('/pacosako/api/meta/:gameId', getMetaHandler); + +/* TODO: Add APIs for posting updates. */ +app.use('/pacosako/api/posttest', express.json()); +app.post('/pacosako/api/posttest', async function(req, res, next) { + try { + res.json(req.body); + } catch (err) { + internalErrorJson(err, res); + } +}); + +app.use('/pacosako/api', express.static('public')); + +app.use('/gun', Gun.serve(__dirname)); + +config.server.listen(config.port); diff --git a/package-lock.json b/package-lock.json index 03e92fb..0b52f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1201,6 +1201,11 @@ "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, "array-map": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", @@ -1463,6 +1468,52 @@ "file-uri-to-path": "1.0.0" } }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "bplist-creator": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", @@ -1848,6 +1899,19 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -1856,6 +1920,16 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -2247,6 +2321,50 @@ "base64-js": "^1.3.0" } }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2528,6 +2646,11 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -3402,6 +3525,11 @@ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -4078,6 +4206,11 @@ "object-visit": "^1.0.0" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", @@ -4086,6 +4219,11 @@ "mimic-fn": "^1.0.0" } }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, "merge-stream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", @@ -4094,6 +4232,11 @@ "readable-stream": "^2.0.1" } }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, "metro": { "version": "0.56.4", "resolved": "https://registry.npmjs.org/metro/-/metro-0.56.4.tgz", @@ -5024,6 +5167,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -5199,6 +5347,15 @@ "react-is": "^16.8.1" } }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -5254,6 +5411,41 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -6450,6 +6642,15 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 28aa295..05a2cac 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "core-js": "^3.6.4", + "express": "^4.17.1", "gun": "^0.2020.301", "gun-db": "^1.0.571", "react": "^16.9.0", diff --git a/public/poll.html b/public/poll.html new file mode 100644 index 0000000..98a8f9e --- /dev/null +++ b/public/poll.html @@ -0,0 +1,73 @@ + + + + Polling Test + + +
+ + + + + +