diff --git a/client/package-lock.json b/client/package-lock.json
index 37f90c3..1ab4509 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -13066,6 +13066,11 @@
"warning": "^3.0.0"
}
},
+ "react-use-websocket": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-2.7.1.tgz",
+ "integrity": "sha512-n0H429jQGaZAnn8BJljCcAgeSezXYzTLO0tb5QvdZnNjhAA8Br3A3GjU5ziNMlxant++Iv/IRQRNMKCfWnsHrQ=="
+ },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index 13395a9..d340ae3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,6 +16,7 @@
"react-scripts": "4.0.3",
"react-stack-grid": "^0.7.1",
"react-textarea-autosize": "^8.3.3",
+ "react-use-websocket": "^2.7.1",
"remark-gfm": "^1.0.0",
"web-vitals": "^0.2.4",
"workbox-background-sync": "^5.1.4",
diff --git a/client/src/Hooks/useDebouncedEffect.hook.js b/client/src/Hooks/useDebouncedEffect.hook.js
deleted file mode 100644
index 1d61219..0000000
--- a/client/src/Hooks/useDebouncedEffect.hook.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/** @file useDebouncedEffect.hook.js */
-import { useState, useEffect } from 'react';
-
-/**
- * Хук debounce
- * @param {*} value
- * @param {*} delay
- */
-function useDebounce(value, delay) {
- const [debouncedValue, setDebouncedValue] = useState(value);
- useEffect(
- () => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
- return () => {
- clearTimeout(handler);
- };
- },
- [delay, value]
- );
- return debouncedValue;
-}
-
-/**
- * Хук debounced effect
- * @param {*} func
- * @param {*} deps
- * @param {*} delay
- */
-function useDebouncedEffect(func, deps, delay) {
- const debounced = useDebounce(deps, delay || 0)
- const debDepsList = Array.isArray(debounced) ? debounced : debounced ? [debounced] : undefined
- useEffect(func, debDepsList) // eslint-disable-line react-hooks/exhaustive-deps
-}
-
-export default useDebouncedEffect
\ No newline at end of file
diff --git a/client/src/Hooks/useDebouncedFunction.hook.js b/client/src/Hooks/useDebouncedFunction.hook.js
new file mode 100644
index 0000000..c4771f5
--- /dev/null
+++ b/client/src/Hooks/useDebouncedFunction.hook.js
@@ -0,0 +1,23 @@
+/** @file useDebouncedFunction.hook.js */
+import { useEffect, useReducer } from 'react';
+
+/**
+ * Хук Debounced Function
+ * @param {(arg)=>void} callback
+ * @param {number} delayms
+ */
+function useDebouncedFunction(callback, delayms) {
+ const [state, debounced] = useReducer((state, arg) => {
+ return { count: state.count + 1, arg: arg }
+ }, { count: 0, arg: undefined })
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ if (state.count) callback(state.arg)
+ }, delayms)
+ return () => clearTimeout(handler)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state])
+ return debounced
+}
+
+export default useDebouncedFunction
\ No newline at end of file
diff --git a/client/src/Hooks/useUpdaterSocket.hook.js b/client/src/Hooks/useUpdaterSocket.hook.js
new file mode 100644
index 0000000..97f68c8
--- /dev/null
+++ b/client/src/Hooks/useUpdaterSocket.hook.js
@@ -0,0 +1,81 @@
+/**@file useUpdaterSocket.hook.js */
+import { useCallback } from "react"
+import useWebSocket from 'react-use-websocket'
+import useDebouncedFunction from "./useDebouncedFunction.hook"
+import { useHttp } from "./http.hook"
+
+const WS_PORT = process.env.WS_PORT || 3030
+
+/**
+ * Хук веб-сокета обновления данных
+ * @param {void} updateData
+ * @param {*} auth
+ */
+function useUpdaterSocket(updateData, auth) {
+ const { request } = useHttp()
+
+ /**дебонсированный обработчик входящего обновления */
+ const debouncedUpdate = useDebouncedFunction(updateData, 200)
+
+ /**колбек для получения url сокета */
+ const getSocketUrl = useCallback(async () => {
+ return new Promise(resolve => {
+ request("/getIp")
+ .then((data) => {
+ const ip = data.ip
+ const socketAddress = "ws://" + (ip || "localhost") + ":" + WS_PORT
+ console.log("socketAddress", socketAddress)
+ resolve(socketAddress)
+ })
+ .catch((err) => console.log(err))
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ /**опции обработки событий сокета */
+ const socketOptions = {
+ onOpen: (e) => {
+ sendRegisterMsg()
+ console.log("ws open")
+ },
+ onClose: (e) => {
+ console.error("ws close")
+ },
+ onMessage: (e) => {
+ console.log("ws update message")
+ debouncedUpdate()
+ },
+ onError: (e) => {
+ console.error("ws error", e)
+ },
+ }
+
+ /**подключение WebSocket */
+ const { sendMessage } = useWebSocket(getSocketUrl, socketOptions)
+
+ /**дебонсированный обработчик отсылаемого обновления */
+ const sendUpdateMsgDebounced = useDebouncedFunction(sendUpdateMsg, 200)
+
+ /** отпрака сообщения сокета */
+ function sendMsg(target) {
+ const msg = JSON.stringify({
+ userId: auth.userId,
+ target
+ })
+ sendMessage(msg)
+ }
+
+ /**отправка отсылаемого обновления */
+ function sendUpdateMsg() {
+ sendMsg("update")
+ }
+
+ /**отпрака сообщения регистрации сокета */
+ function sendRegisterMsg() {
+ sendMsg("register")
+ }
+
+ return [sendUpdateMsgDebounced]
+}
+
+export default useUpdaterSocket
\ No newline at end of file
diff --git a/client/src/NoteComponents/ModalNoteEdit.js b/client/src/NoteComponents/ModalNoteEdit.js
index f810759..42fcfe9 100644
--- a/client/src/NoteComponents/ModalNoteEdit.js
+++ b/client/src/NoteComponents/ModalNoteEdit.js
@@ -25,10 +25,10 @@ function calcMaxRows() {
*/
function ModalNoteEdit() {
/**получение контекста */
- const { removeNote, changeNoteColor, unsetEditNoteId, editNoteContent, getNoteByIndex, editNoteId } = React.useContext(NotesContext)
+ const { removeNote, changeNoteColor, unsetEditNoteId, editNoteContent, getNoteById, editNoteId } = React.useContext(NotesContext)
/** обьект заметки */
- const note = getNoteByIndex(editNoteId)
+ const note = getNoteById(editNoteId)
React.useEffect(() => { if (note !== null) open() }, [note])
/**хук состояния формы */
diff --git a/client/src/NoteComponents/NoteItem.js b/client/src/NoteComponents/NoteItem.js
index ec836dd..1e9adcb 100644
--- a/client/src/NoteComponents/NoteItem.js
+++ b/client/src/NoteComponents/NoteItem.js
@@ -2,7 +2,6 @@
* @file NoteItem.js
*/
import React, { useContext } from 'react'
-import PropTypes from 'prop-types'
import NotesContext from '../Context/NotesContext'
import Note, { PropTypeNote } from '../Shared/noteType/Note'
import ReactMarkdown from 'react-markdown'
@@ -18,7 +17,7 @@ function fixLineBreaks(mdStr) {
* @param {*} param0
*
*/
-function NoteItem({ note = new Note(), index }) {
+function NoteItem({ note = new Note() }) {
/**Подключение контекста */
const { setEditNoteId, editNoteOrder } = useContext(NotesContext)
@@ -39,7 +38,7 @@ function NoteItem({ note = new Note(), index }) {
{/**Заголовок и текст заметки с обработчиками отображения markdown*/}
-
setEditNoteId(index)} >
+
setEditNoteId(note.id)} >
@@ -56,14 +55,14 @@ function NoteItem({ note = new Note(), index }) {
@@ -75,8 +74,7 @@ function NoteItem({ note = new Note(), index }) {
// Валидация
NoteItem.propTypes = {
- note: PropTypeNote.isRequired,
- index: PropTypes.number
+ note: PropTypeNote.isRequired
}
export default NoteItem
diff --git a/client/src/NoteComponents/NoteList.js b/client/src/NoteComponents/NoteList.js
index 1c6e3f5..d90b2f1 100644
--- a/client/src/NoteComponents/NoteList.js
+++ b/client/src/NoteComponents/NoteList.js
@@ -54,9 +54,9 @@ function NoteList(props) {
{/**Отзывчивая сетка карточек */}
{/**Рендер каждой карточки из массива */}
- {Array.isArray(props.notes) ? (props.notes.sort(sortByOrder).reverse().map((note, index) => {
+ {Array.isArray(props.notes) ? (props.notes.sort(sortByOrder).reverse().map((note) => {
return (
-
+
)
})) : null}
diff --git a/client/src/Pages/NotesPage.js b/client/src/Pages/NotesPage.js
index 5ef4b04..3e0b425 100644
--- a/client/src/Pages/NotesPage.js
+++ b/client/src/Pages/NotesPage.js
@@ -17,6 +17,7 @@ import useFetchNotes from '../Hooks/useFetchNotes.hook'
import useEditNoteId from '../Hooks/useEditNoteId.hook'
import useNotesArr from '../Hooks/useNotesArr.hook'
import { calcOrder, fixOrders } from '../Shared/order'
+import useUpdaterSocket from '../Hooks/useUpdaterSocket.hook'
/**
* Страница с заметками
@@ -52,6 +53,9 @@ function NotesPage() {
/** подключение контроллера обновления данных */
const [updateData] = useDataLoadingController(loadDataFromServer, setLoadedNotes, auth, 60)
+ /** подключение сокета обновления данных */
+ const [sendUpdateMsg] = useUpdaterSocket(updateData, auth)
+
///////////
/**
@@ -67,14 +71,14 @@ function NotesPage() {
* @param {string} target
*/
function loadDataToServer(note = new Note(), target = 'set') {
- fetchNotes(target, "POST", { note })
+ fetchNotes(target, "POST", { note }, sendUpdateMsg)
}
///////////
/**
* Внесение в полученных данных в массив
- * @param {*} notes
+ * @param {Array<{}>} notes
*/
function setLoadedNotes(notes) {
setNotesArr([...notes])
@@ -82,9 +86,10 @@ function NotesPage() {
/**
* удаление карточки
- * @param {*} index
+ * @param {string} id
*/
- function removeNote(index) {
+ function removeNote(id) {
+ const index = getNoteIndexById(id)
const toDelete = notesArr.splice(index, 1)[0]
setNotesArr([...notesArr])
loadDataToServer(toDelete, "delete")
@@ -92,7 +97,7 @@ function NotesPage() {
/**
* добавление карточки
- * @param {*} noteData
+ * @param {{}} noteData
*/
function addNote(noteData = {}) {
const newId = String(auth.email) + String(Date.now()) + String(Math.random())
@@ -103,21 +108,20 @@ function NotesPage() {
text: noteData.text,
order: calcOrder(notesArr)
})
- //console.log(newId, newNote.id);
- const newIndex = (notesArr != null) ? notesArr.length : 0
setNotesArr(
(notesArr != null) ? notesArr.concat([newNote]) : [newNote]
)
loadDataToServer(newNote, "set")
- setEditNoteId(newIndex)
+ setEditNoteId(newId)
}
/**
* Изменение цвета карточки
- * @param {*} index
- * @param {*} color
+ * @param {string} id
+ * @param {string} color
*/
- function changeNoteColor(index, color) {
+ function changeNoteColor(id, color) {
+ const index = getNoteIndexById(id)
notesArr[index].color = color
setNotesArr([...notesArr])
loadDataToServer(notesArr[index], "set")
@@ -125,12 +129,13 @@ function NotesPage() {
/**
* Изменение текстового содержания карточки
- * @param {*} index
- * @param {*} name
- * @param {*} text
+ * @param {string} id
+ * @param {string} name
+ * @param {string} text
*/
- function editNoteContent(index, name, text) {
- if (notesArr[index]) {
+ function editNoteContent(id, name, text) {
+ const index = getNoteIndexById(id)
+ if (index !== null) {
let note = new Note(notesArr[index])
note.name = name
note.text = text
@@ -142,11 +147,12 @@ function NotesPage() {
/**
* Изменение порядка заметки
- * @param {number} index
+ * @param {string} id
* @param {boolean} orderOperationFlag
*/
- function editNoteOrder(index, orderOperationFlag) {
- if (notesArr[index]) {
+ function editNoteOrder(id, orderOperationFlag) {
+ const index = getNoteIndexById(id)
+ if (index !== null) {
notesArr[index].order += orderOperationFlag ? 1 : -1
let fixedArr = fixOrders(notesArr)
setNotesArr(fixedArr)
@@ -156,9 +162,40 @@ function NotesPage() {
}
}
- /**функция получения карточки по id */
- function getNoteByIndex(index) {
- return index !== null ? notesArr[index] : null
+ ///////////
+
+ /**
+ * функция получения карточки по id
+ * @param {string} id
+ */
+ function getNoteById(id) {
+ const byId = () => {
+ let note = null
+ if (Array.isArray(notesArr)) {
+ notesArr.forEach((val, index) => {
+ if (val.id === id) note = val
+ })
+ }
+ return note
+ }
+ return id !== null ? byId() : null
+ }
+
+ /**
+ * функция получения индекса карточки по id
+ * @param {string} id
+ */
+ function getNoteIndexById(id) {
+ const byId = () => {
+ let index = null
+ if (Array.isArray(notesArr)) {
+ notesArr.forEach((val, ind) => {
+ if (val.id === id) index = ind
+ })
+ }
+ return index
+ }
+ return id !== null ? byId() : null
}
///////////
@@ -182,7 +219,7 @@ function NotesPage() {
/**рендер */
return (
/**Здесь отрисовываются меню добавления и редактирования заметок и сам перечнь заметок в виде динамичной отзывчивой сетки */
-
+
{/**Компонент добавления карточки и модальное окно редактирования */}
diff --git a/jsdoc.json b/jsdoc.json
index ace4c21..65af500 100644
--- a/jsdoc.json
+++ b/jsdoc.json
@@ -6,6 +6,7 @@
"server/routes",
"server/models",
"server/middleware",
+ "server/socket",
"client",
"client/src",
"client/src/Shared",
diff --git a/package.json b/package.json
index 7f61485..8109031 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"dev": "cross-env NODE_ENV=dev concurrently \"npm run server:start-dev\" \"npm run client:start-dev\"",
"prod": "npm run server:start-prod",
"prod:predeploy": "npm run deploy && npm run prod",
+ "prod:prebuild": "npm run client:build && npm run prod",
"test": "echo \"Error: no test specified\" && exit 1",
"heroku-postbuild": "npm run deploy",
"mongo-ubuntu:update": "(sudo apt update && sudo apt upgrade)",
diff --git a/server/app.js b/server/app.js
index c369222..94b155e 100644
--- a/server/app.js
+++ b/server/app.js
@@ -5,30 +5,36 @@
const express = require('express')
const path = require('path')
-const dns = require('dns')
const os = require('os')
const mongoose = require('mongoose')
const https = require('./middleware/https.middleware')
+const startWSS = require('./socket/wss')
require('dotenv').config()
-/**
- * подключение переменных среды
- */
+//подключение переменных среды
const devMode = process.env.NODE_ENV === "dev"
const PORT = process.env.PORT || 5000
const mongoUri = process.env.mongoUri
const httpsRedirect = process.env.httpsRedirect || false
+const WS_PORT = process.env.WS_PORT || 3030
const app = express()
app.use(express.json({ extended: true }))
+app.get('/getIp', (req, res) => {
+ getIp()
+ .then((ip) => res.status(200).json({ ip }))
+ .catch(() => res.status(500))
+})
+
/**
* подключение роутов
*/
app.use('/api/auth', require('./routes/auth.routes'))
app.use('/api/notes', require('./routes/notes.routes'))
+
if (httpsRedirect) app.use(https)
/**
@@ -52,6 +58,7 @@ if (!devMode) {
async function start() {
try {
connectMongo(mongoUri)
+ startWSS(WS_PORT)
app.listen(PORT, logServerStart)
} catch (e) {
console.log('Server Error', e.message)
@@ -80,12 +87,22 @@ async function connectMongo(mongoUri) {
/**
* Вывод информации о сервере
*/
-function logServerStart() {
- dns.lookup(os.hostname(), (err, address) => {
- const [logName, sBef, sAft] = devMode ? ['Express server', ' ', ':'] : ['React Notes App', '-', '']
- console.log(`\n${logName} has been started`)
- console.log(`${sBef} Local${sAft} http://localhost:${PORT}`)
- console.log(`${sBef} On Your Network${sAft} http://${address}:${PORT}`)
- if (err) console.log(err)
+async function logServerStart() {
+ const [logName, sBef, sAft] = devMode ? ['Express server', ' ', ':'] : ['React Notes App', '-', '']
+ const ip = await getIp()
+ console.log(`\n${logName} has been started`)
+ console.log(`${sBef} Local${sAft} http://localhost:${PORT}`)
+ console.log(`${sBef} On Your Network${sAft} http://${ip}:${PORT}\n`)
+}
+
+/**
+ * Получение ip сервера
+ */
+function getIp() {
+ return new Promise((res, rej) => {
+ require('dns').lookup(os.hostname(), (err, addr) => {
+ addr ? res(addr) : rej()
+ err && console.log(err)
+ })
})
}
diff --git a/server/package-lock.json b/server/package-lock.json
index 9fba14c..77cf659 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1669,6 +1669,11 @@
"typedarray-to-buffer": "^3.1.5"
}
},
+ "ws": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+ "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
+ },
"xdg-basedir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
diff --git a/server/package.json b/server/package.json
index fa21c55..f167922 100644
--- a/server/package.json
+++ b/server/package.json
@@ -16,9 +16,10 @@
"express": "^4.17.1",
"express-validator": "^6.11.1",
"jsonwebtoken": "^8.5.1",
- "mongoose": "^5.12.13"
+ "mongoose": "^5.12.13",
+ "ws": "^7.4.6"
},
"devDependencies": {
"nodemon": "^2.0.7"
}
-}
\ No newline at end of file
+}
diff --git a/server/socket/wss.js b/server/socket/wss.js
new file mode 100644
index 0000000..8bfe20a
--- /dev/null
+++ b/server/socket/wss.js
@@ -0,0 +1,79 @@
+/**@file wss.js */
+const WebSocket = require('ws')
+
+/**
+ * Подключение WebSocket сервера
+ * Он принимает сигналы об обновлении от клиента и рассылает остальным для синхронизации
+ * @param {*} port
+ */
+function startWSS(port) {
+ /**сервер ws */
+ const wsServer = new WebSocket.Server({ port })
+ /**коллекция ws клиентов разделенная по id пользователя */
+ const wsCollection = {}
+ // обработка событий сервера
+ wsServer.on('connection', (wsClient) => {
+ // уникальный номер клиента
+ const clientNum = Date.now()
+ // проверка регистрации
+ setTimeout(() => {
+ if (!wsClient.userId) wsClient.close()
+ }, 60 * 1000)
+ // обработка сообщения клиента
+ wsClient.on("message", (data) => {
+ try {
+ // данные с клиента
+ const { userId, target } = JSON.parse(data)
+ // обработка сообщения регистрации
+ if (target == "register") {
+ wsClient.userId = userId
+ wsClient.num = clientNum
+ let clients = getClients(wsClient.userId)
+ let match = false
+ clients.forEach(val => {
+ if (val.num == wsClient.num) match = true
+ })
+ if (!match && wsClient.userId) {
+ clients.push({
+ num: wsClient.num,
+ send: msg => wsClient.send(msg)
+ })
+ wsCollection[wsClient.userId] = clients
+ } else throw new Error("ошибка регистрации сокета")
+ }
+ // обработка сообщения об обновлении
+ if (target == "update" && wsClient.userId) {
+ getClients(wsClient.userId).forEach((wsc) => {
+ if ((wsClient.num !== undefined) && (wsClient.num !== wsc.num)) {
+ wsc.send(data)
+ }
+ })
+ }
+ } catch (e) { wsClient.close() }
+ })
+ // обработка закрытия клиента
+ wsClient.on("close", () => {
+ if (wsClient.userId) wsCollection[wsClient.userId] = getClients(wsClient.userId).filter((wsc) => {
+ return (wsClient.num === undefined) || (wsClient.num !== wsc.num)
+ })
+ })
+ // обработка ошибки клиента
+ wsClient.on("error", () => {
+ wsClient.close()
+ })
+ })
+ wsServer.on("close", () => console.log("WSS closed"))
+ wsServer.on("error", () => console.log("WSS error"))
+ wsServer.on("listening", () => console.log("WSS listening"))
+
+ /**
+ * Получение колекции соединений по id
+ * @param {string} userId
+ * @returns {Array}
+ */
+ function getClients(userId) {
+ return wsCollection[userId] || []
+ }
+}
+
+module.exports = startWSS
\ No newline at end of file