chat persistence

cordova
Valerie 2022-07-21 04:18:39 -04:00
parent 7d49867b14
commit d50029146c
18 changed files with 322 additions and 33 deletions

View File

@ -1,12 +1,14 @@
version: '3.1'
services:
db:
image: mariadb
restart: always
environment:
MARIADB_ROOT_PASSWORD: example
MARIADB_ROOT_HOST: '127.0.0.1'
ports:
- 3306:3306
adminer:
image: adminer

101
package-lock.json generated
View File

@ -1,12 +1,13 @@
{
"name": "vite-electron-builder",
"name": "viscord",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vite-electron-builder",
"name": "viscord",
"hasInstallScript": true,
"dependencies": {
"@types/mysql": "^2.15.21",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3",
@ -17,6 +18,7 @@
"eslint-plugin-react": "^7.30.1",
"express": "^4.18.1",
"get-port": "^6.1.2",
"mysql": "^2.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-time-ago": "^7.2.1",
@ -914,6 +916,14 @@
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"node_modules/@types/mysql": {
"version": "2.15.21",
"resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.21.tgz",
"integrity": "sha512-NPotx5CVful7yB+qZbWtXL2fA4e7aEHkihHLjklc6ID8aq7bhguHgeIoC1EmSNTAuCgI6ZXrjt2ZSaXnYX0EUg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "16.11.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.39.tgz",
@ -1883,6 +1893,14 @@
}
]
},
"node_modules/bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -2599,8 +2617,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/crc": {
"version": "3.8.0",
@ -5203,8 +5220,7 @@
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/isbinaryfile": {
"version": "4.0.10",
@ -5672,6 +5688,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"dependencies": {
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nano-staged": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz",
@ -6187,8 +6217,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/progress": {
"version": "2.0.3",
@ -6436,7 +6465,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -6950,6 +6978,14 @@
"dev": true,
"optional": true
},
"node_modules/sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stat-mode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
@ -6971,7 +7007,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@ -7694,8 +7729,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/utils-merge": {
"version": "1.0.1",
@ -8823,6 +8857,14 @@
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"@types/mysql": {
"version": "2.15.21",
"resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.21.tgz",
"integrity": "sha512-NPotx5CVful7yB+qZbWtXL2fA4e7aEHkihHLjklc6ID8aq7bhguHgeIoC1EmSNTAuCgI6ZXrjt2ZSaXnYX0EUg==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "16.11.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.39.tgz",
@ -9544,6 +9586,11 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -10082,8 +10129,7 @@
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"crc": {
"version": "3.8.0",
@ -11940,8 +11986,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "4.0.10",
@ -12309,6 +12354,17 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"requires": {
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
}
},
"nano-staged": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz",
@ -12684,8 +12740,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"progress": {
"version": "2.0.3",
@ -12871,7 +12926,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -13259,6 +13313,11 @@
"dev": true,
"optional": true
},
"sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="
},
"stat-mode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
@ -13274,7 +13333,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -13813,8 +13871,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",

View File

@ -55,6 +55,7 @@
"vue-tsc": "0.38.8"
},
"dependencies": {
"@types/mysql": "^2.15.21",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-timeago": "^4.1.3",
@ -65,6 +66,7 @@
"eslint-plugin-react": "^7.30.1",
"express": "^4.18.1",
"get-port": "^6.1.2",
"mysql": "^2.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-time-ago": "^7.2.1",

View File

@ -48,8 +48,19 @@ const connect = async () => {
connect();
export function send(action: string, data?: any) {
export async function send(action: string, data?: any) {
if(socket === null) return;
if(socket && socket.readyState === socket.CONNECTING) {
try {
await new Promise((resolve, reject) => {
socket?.addEventListener('open', resolve);
socket?.addEventListener('close', reject);
});
} catch(e) {
return;
}
if(socket.readyState !== socket.OPEN) return;
}
const message = JSON.stringify({ action, data });
socket.send(message);
}

View File

@ -49,6 +49,9 @@ export default () => {
message(data: Message) {
setMessages([...messages, data]);
},
recent(data: { messages: Message[] }) {
setMessages(data.messages);
},
});
registerRouter(actions);
return () => {
@ -56,9 +59,16 @@ export default () => {
};
}, [messages]);
useEffect(() => {
if(messages.length === 0) {
console.log('sending recents request');
send('recent');
}
}, [messages]);
const sendMessage = useCallback(() => {
if(textBoxRef.current === null) return;
send('message', createMessage('Version 3', textBoxRef.current.innerText));
send('message', createMessage('Val', textBoxRef.current.innerText));
textBoxRef.current.innerText = '';
}, []);
@ -93,7 +103,7 @@ export default () => {
width: '100%',
}}>
{messages.map(message => (
<div style={{
<div key={message.uid} style={{
display: 'grid',
gridTemplateColumns: '128px 1fr',
width: '100%',

View File

@ -0,0 +1,4 @@
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=example
DB_DB=viscord

View File

@ -0,0 +1,27 @@
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
SET NAMES utf8mb4;
CREATE DATABASE `viscord` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `viscord`;
CREATE TABLE `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` tinytext NOT NULL,
`from` tinytext NOT NULL,
`text` longtext NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `migrations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `migrations` ()
VALUES ();
-- 2022-07-21 01:27:49

View File

@ -0,0 +1,8 @@
USE `viscord`;
ALTER TABLE `messages`
ADD COLUMN `t_sent` BIGINT UNSIGNED AFTER `text`;
INSERT INTO `migrations` ()
VALUES ();

View File

@ -0,0 +1,106 @@
// import migration from './migrations/1-chat-persistence.sql';
import { createConnection } from 'mysql';
import { readdirSync, readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const host = 'localhost';
const user = 'root';
const password = 'example';
interface Migration {
sql: string;
version: number;
}
const migrations: Migration[] =
readdirSync(resolve(__dirname, 'migrations'))
.sort((a, b) => {
const an = Number(a.split('-')[0]);
const bn = Number(b.split('-')[0]);
return an > bn ? 1 : an < bn ? -1 : 0;
})
.map(path => {
const fullpath = resolve(__dirname, 'migrations', path);
const n = Number(path.split('-')[0]);
return {
sql: readFileSync(fullpath).toString(),
version: n,
};
});
export const connection = createConnection({
host,
user,
password,
});
const migrationConnection = createConnection({
host,
user,
password,
multipleStatements: true,
});
const connected: Promise<null> = new Promise((res, rej) => {
connection.connect((err) => {
if(err === null) {
console.log('connected to database!');
res(null);
} else {
console.error(err);
rej(err);
}
});
});
export async function update() {
// console.log(migrations);
console.log('waiting for connection...');
await connected;
// determine version
const currentVersion: number = await new Promise((resolve, rej) => {
connection.query(`
SELECT SCHEMA_NAME
FROM INFORMATION_SCHEMA.SCHEMATA
WHERE SCHEMA_NAME = 'viscord'
`, async (err, res, fields) => {
if(res.length === 0) {
resolve(0);
} else {
const version: number = await new Promise((resolve, reject) => {
connection.query(`
SELECT max(id) as 'version' FROM viscord.migrations;
`, function (err, results, fields) {
resolve(results[0].version);
});
});
resolve(version);
// check the version table!
}
});
});
const expectedVersion = migrations.length;
if(currentVersion >= expectedVersion) {
console.log('database up to date!');
} else {
const difference = expectedVersion - currentVersion;
console.log(`database ${difference} version${difference !== 1 ? 's' : ''} behind`);
// console.log(`${currentVersion} >>> ${expectedVersion}`);
const neededMigrations = migrations.filter(m => m.version > currentVersion);
for(const migration of neededMigrations) {
console.log(`${currentVersion} >>> ${migration.version}`);
await new Promise((resolve, reject) => {
migrationConnection.query(migration.sql, (err, res) => {
if(err !== null) return reject(err);
console.log(`executed ${res.length} statement${res.length !== 0 ? 's' : ''}`);
});
});
}
}
// console.log('database version:', currentVersion)
// console.log(response);
}

View File

@ -0,0 +1,16 @@
import { connection } from './migrate';
export default async function(a: any, ...opts: any[]): Promise<any[]> {
const b64 = a.split('base64,')[1];
const text = Buffer.from(b64, 'base64').toString();
return await new Promise((resolve, reject) => {
connection.query(text, [...opts], (err, results) => {
if(!err) return resolve(results);
console.error(err);
});
});
// console.log(...opts)
}

View File

@ -0,0 +1,8 @@
INSERT INTO viscord.messages
(`text`, `from`, `uid`, `t_sent`)
VALUES (
?,
?,
?,
UNIX_TIMESTAMP()
)

View File

@ -0,0 +1,3 @@
SELECT * FROM viscord.messages
ORDER BY t_sent
LIMIT 100;

View File

@ -8,7 +8,7 @@ const wss = new WebSocketServer({
});
wss.on('connection', (ws) => {
ws.on('message', (str) => {
ws.on('message', async (str) => {
try {
const message = JSON.parse(str.toString());
if(typeof message.action !== 'string') {
@ -17,7 +17,10 @@ wss.on('connection', (ws) => {
}
const {action, data} = message;
try {
router(action, data);
const _return = await (router(action, data) as unknown as Promise<any>);
if(_return) {
ws.send(JSON.stringify(_return));
}
} catch(e) {
console.warn(`error in action ${action}`);
console.error(e);
@ -39,4 +42,14 @@ export function broadcast(action: string, data?: any) {
}
}
export default wss;
export default wss;
// -------------
import { update } from './db/migrate';
try {
update();
} catch (e) {
console.error(e);
}

View File

@ -11,7 +11,7 @@ export default function router(routes: any) {
}
return function(route: any, data: any) {
if(route in routes) {
routes[route](data);
return routes[route](data);
} else {
console.warn(`route <${route}> not found`);
}

View File

@ -1,11 +1,30 @@
import router from '../router';
import { broadcast } from '../index';
import query from '../db/query';
import newMessage from '../db/snippets/message/new.sql';
import recentMessages from '../db/snippets/message/recent.sql';
export default router({
up() {
console.log(Date.now());
},
message(data: string) {
message(data: any) {
query(newMessage, data.text, data.from, data.uid, data.timestamp);
broadcast('message', data);
},
async recent() {
console.log('got recents request');
const messages = await query(recentMessages);
return {
action: 'recent',
data: {
messages: messages.map(v => ({
from: v.from,
uid: v.uid,
timestamp: v.t_sent * 1000,
text: v.text,
})),
},
};
},
});

View File

@ -3,7 +3,6 @@ import {join} from 'path';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
@ -17,6 +16,9 @@ const config = {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
assetsInclude: [
'**/*.sql',
],
build: {
ssr: true,
sourcemap: 'inline',

View File

@ -58,7 +58,7 @@ const setupServerPackageWatcher = () => {
});
/** Proxy all logs */
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.info(d.toString().trim(), {timestamp: true}));
/** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
spawnProcess.stderr.on('data', d => {

1
types/sqlshim.d.ts vendored 100644
View File

@ -0,0 +1 @@
declare module '*.sql';