diff --git a/.DS_Store b/.DS_Store index 90d1f2d2..b25b8769 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/server/package.json b/server/package.json index d8bf2141..e56e55d8 100644 --- a/server/package.json +++ b/server/package.json @@ -11,8 +11,11 @@ "nodemon": "nodemon dist/server.js" }, "dependencies": { + "@codemirror/collab": "^0.19.0", + "@codemirror/state": "^0.19.9", "cors": "^2.8.5", "express": "^4.17.3", + "socket.io": "^4.4.1", "typescript": "^4.6.2" }, "devDependencies": { diff --git a/server/src/events.ts b/server/src/events.ts new file mode 100644 index 00000000..113e36aa --- /dev/null +++ b/server/src/events.ts @@ -0,0 +1,5 @@ +export const serverEvents = { + openPage: "openPage", + closePage: "closePage", + pageText: "pageText", +}; diff --git a/server/src/server.ts b/server/src/server.ts index 85b579c4..1f7f7fd2 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,10 +4,23 @@ import fs from "fs"; import { readdir, readFile, stat, unlink } from "fs/promises"; import path from "path"; import stream from "stream"; -import {} from "stream/promises"; import { promisify } from "util"; +import { ChangeSet, Text } from "@codemirror/state"; +import { Update } from "@codemirror/collab"; +import http from "http"; +import { Server } from "socket.io"; +import { serverEvents } from "./events"; + const app = express(); +const server = http.createServer(app); +const io = new Server(server, { + cors: { + methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE", + preflightContinue: true, + }, +}); + const port = 3000; const pipeline = promisify(stream.pipeline); const pagesPath = "../pages"; @@ -18,34 +31,88 @@ type PageMeta = { lastModified: number; }; -app.use("/", express.static(distDir)); +class DiskFS { + rootPath: string; -let fsRouter = express.Router(); + constructor(rootPath: string) { + this.rootPath = rootPath; + } -// Page list -fsRouter.route("/").get(async (req, res) => { - const localPath = pagesPath; - let fileNames: PageMeta[] = []; + async listPages(): Promise { + let fileNames: PageMeta[] = []; - async function walkPath(dir: string) { - let files = await readdir(dir); - for (let file of files) { - const fullPath = path.join(dir, file); - let s = await stat(fullPath); - if (s.isDirectory()) { - await walkPath(fullPath); - } else { - if (path.extname(file) === ".md") { - fileNames.push({ - name: fullPath.substring(pagesPath.length + 1, fullPath.length - 3), - lastModified: s.mtime.getTime(), - }); + let _this = this; + + async function walkPath(dir: string) { + let files = await readdir(dir); + for (let file of files) { + const fullPath = path.join(dir, file); + let s = await stat(fullPath); + if (s.isDirectory()) { + await walkPath(fullPath); + } else { + if (path.extname(file) === ".md") { + fileNames.push({ + name: fullPath.substring( + _this.rootPath.length + 1, + fullPath.length - 3 + ), + lastModified: s.mtime.getTime(), + }); + } } } } + await walkPath(this.rootPath); + return fileNames; } - await walkPath(pagesPath); - res.json(fileNames); + + async readPage(pageName: string): Promise<{ text: string; meta: PageMeta }> { + const localPath = path.join(pagesPath, pageName + ".md"); + const s = await stat(localPath); + return { + text: await readFile(localPath, "utf8"), + meta: { + name: pageName, + lastModified: s.mtime.getTime(), + }, + }; + } + + async writePage(pageName: string, body: any): Promise { + let localPath = path.join(pagesPath, pageName + ".md"); + await pipeline(body, fs.createWriteStream(localPath)); + console.log(`Wrote to ${localPath}`); + const s = await stat(localPath); + return { + name: pageName, + lastModified: s.mtime.getTime(), + }; + } + + async getPageMeta(pageName: string): Promise { + let localPath = path.join(pagesPath, pageName + ".md"); + const s = await stat(localPath); + return { + name: pageName, + lastModified: s.mtime.getTime(), + }; + } + + async deletePage(pageName: string) { + let localPath = path.join(pagesPath, pageName + ".md"); + await unlink(localPath); + } +} + +app.use("/", express.static(distDir)); + +let fsRouter = express.Router(); +let diskFS = new DiskFS(pagesPath); + +// Page list +fsRouter.route("/").get(async (req, res) => { + res.json(await diskFS.listPages()); }); fsRouter @@ -54,13 +121,11 @@ fsRouter let reqPath = req.params[0]; console.log("Getting", reqPath); try { - const localPath = path.join(pagesPath, reqPath + ".md"); - const s = await stat(localPath); - let content = await readFile(localPath, "utf8"); + let { text, meta } = await diskFS.readPage(reqPath); res.status(200); - res.header("Last-Modified", "" + s.mtime.getTime()); + res.header("Last-Modified", "" + meta.lastModified); res.header("Content-Type", "text/markdown"); - res.send(content); + res.send(text); } catch (e) { res.status(200); res.send(""); @@ -69,14 +134,10 @@ fsRouter .put(async (req, res) => { let reqPath = req.params[0]; - let localPath = path.join(pagesPath, reqPath + ".md"); - try { - await pipeline(req, fs.createWriteStream(localPath)); - console.log(`Wrote to ${localPath}`); - const s = await stat(localPath); + let meta = await diskFS.writePage(reqPath, req); res.status(200); - res.header("Last-Modified", "" + s.mtime.getTime()); + res.header("Last-Modified", "" + meta.lastModified); res.send("OK"); } catch (err) { res.status(500); @@ -87,11 +148,9 @@ fsRouter .options(async (req, res) => { let reqPath = req.params[0]; try { - const localPath = path.join(pagesPath, reqPath + ".md"); - const s = await stat(localPath); + const meta = await diskFS.getPageMeta(reqPath); res.status(200); - res.header("Last-Modified", "" + s.mtime.getTime()); - res.header("Content-length", "" + s.size); + res.header("Last-Modified", "" + meta.lastModified); res.header("Content-Type", "text/markdown"); res.send(""); } catch (e) { @@ -101,13 +160,12 @@ fsRouter }) .delete(async (req, res) => { let reqPath = req.params[0]; - const localPath = path.join(pagesPath, reqPath + ".md"); try { - await unlink(localPath); + await diskFS.deletePage(reqPath); res.status(200); res.send("OK"); } catch (e) { - console.error("Error deleting file", localPath, e); + console.error("Error deleting file", reqPath, e); res.status(500); res.send("OK"); } @@ -131,6 +189,63 @@ app.get("/*", async (req, res) => { res.status(200).header("Content-Type", "text/html").send(cachedIndex); }); -app.listen(port, () => { +import { Socket } from "socket.io"; + +class Page { + text: Text; + updates: Update[]; + sockets: Map; + + constructor(text: string) { + this.updates = []; + this.text = Text.of([text]); + this.sockets = new Map(); + } +} + +let openPages = new Map(); + +io.on("connection", (socket) => { + function removeSocket(pageName: string) { + let page = openPages.get(pageName); + if (page) { + page.sockets.delete(socket.id); + if (page.sockets.size === 0) { + console.log("No more sockets for", pageName, "flushing"); + openPages.delete(pageName); + } + } + } + + console.log("Connected", socket.id); + let socketOpenPages = new Set(); + socket.on(serverEvents.openPage, async (pageName: string) => { + let page = openPages.get(pageName); + if (!page) { + let { text } = await diskFS.readPage(pageName); + page = new Page(text); + openPages.set(pageName, page); + } + page.sockets.set(socket.id, socket); + socketOpenPages.add(pageName); + console.log("Sending document text"); + socket.emit( + serverEvents.pageText, + pageName, + openPages.get(pageName).text.toJSON() + ); + }); + socket.on(serverEvents.closePage, (pageName: string) => { + console.log("Closing page", pageName); + removeSocket(pageName); + socketOpenPages.delete(pageName); + }); + socket.on("disconnect", () => { + console.log("Disconnected", socket.id); + socketOpenPages.forEach(removeSocket); + }); +}); +//sup +server.listen(port, () => { console.log(`Server istening on port ${port}`); }); diff --git a/server/yarn.lock b/server/yarn.lock index acee30f4..4d92bfb7 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -23,6 +23,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@codemirror/collab@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@codemirror/collab/-/collab-0.19.0.tgz#43938671d58ef8f12e43406ddd315410d85ac1c4" + integrity sha512-pyrEXLLkby82y9wzfanEQGl3V3DX/pIuA97Js7gVEbPAqhvse5iXKNyp1Yr37afhkl2TUeoZyUSFTtcimgdI6g== + dependencies: + "@codemirror/state" "^0.19.0" + +"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.19.9.tgz#b797f9fbc204d6dc7975485e231693c09001b0dd" + integrity sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw== + dependencies: + "@codemirror/text" "^0.19.0" + +"@codemirror/text@^0.19.0": + version "0.19.6" + resolved "https://registry.yarnpkg.com/@codemirror/text/-/text-0.19.6.tgz#9adcbd8137f69b75518eacd30ddb16fd67bbac45" + integrity sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA== + "@parcel/bundler-default@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.3.2.tgz#329f171e210dfb22beaa52ae706ccde1dae384c1" @@ -580,6 +599,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@socket.io/base64-arraybuffer@~1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" + integrity sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ== + "@swc/helpers@^0.2.11": version "0.2.14" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.2.14.tgz#20288c3627442339dd3d743c944f7043ee3590f0" @@ -605,6 +629,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/component-emitter@^1.2.10": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506" + integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ== + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -612,6 +641,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cors@^2.8.12": version "2.8.12" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" @@ -641,7 +675,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@*": +"@types/node@*", "@types/node@>=10.0.0": version "17.0.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== @@ -679,7 +713,7 @@ abortcontroller-polyfill@^1.1.9: resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== -accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -743,6 +777,11 @@ base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -955,6 +994,11 @@ commander@^7.0.0, commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -989,12 +1033,12 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.2: +cookie@0.4.2, cookie@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -cors@^2.8.5: +cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -1124,6 +1168,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.1, debug@~4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -1235,6 +1286,29 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-parser@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" + integrity sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg== + dependencies: + "@socket.io/base64-arraybuffer" "~1.0.2" + +engine.io@~6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.3.tgz#f156293d011d99a3df5691ac29d63737c3302e6f" + integrity sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.2.3" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -1744,6 +1818,11 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -2371,6 +2450,32 @@ signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +socket.io-adapter@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486" + integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ== + +socket.io-parser@~4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" + integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== + dependencies: + "@types/component-emitter" "^1.2.10" + component-emitter "~1.3.0" + debug "~4.3.1" + +socket.io@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.1.tgz#cd6de29e277a161d176832bb24f64ee045c56ab8" + integrity sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.1.0" + socket.io-adapter "~2.3.3" + socket.io-parser "~4.0.4" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -2629,6 +2734,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" diff --git a/webapp/package.json b/webapp/package.json index f7ee3b34..d4f20782 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,6 +17,7 @@ "@parcel/transformer-sass": "2.3.2", "@parcel/transformer-webmanifest": "2.3.2", "@parcel/validator-typescript": "^2.3.2", + "@types/events": "^3.0.0", "@types/node": "^17.0.21", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", @@ -51,6 +52,7 @@ "dexie": "^3.2.1", "idb": "^7.0.0", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "socket.io-client": "^4.4.1" } } diff --git a/webapp/src/boot.ts b/webapp/src/boot.ts index a6116180..8cf63801 100644 --- a/webapp/src/boot.ts +++ b/webapp/src/boot.ts @@ -1,9 +1,14 @@ import { Editor } from "./editor"; import { HttpRemoteSpace } from "./space"; import { safeRun } from "./util"; +import { io } from "socket.io-client"; + +let socket = io("http://localhost:3000"); + +import { serverEvents } from "../../server/src/events"; let editor = new Editor( - new HttpRemoteSpace(`http://${location.hostname}:3000/fs`), + new HttpRemoteSpace(`http://${location.hostname}:3000/fs`, socket), document.getElementById("root")! ); diff --git a/webapp/src/editor.tsx b/webapp/src/editor.tsx index 7bd8d546..64526d06 100644 --- a/webapp/src/editor.tsx +++ b/webapp/src/editor.tsx @@ -43,7 +43,7 @@ import { slashCommandRegexp } from "./types"; import reducer from "./reducer"; import { smartQuoteKeymap } from "./smart_quotes"; -import { Space } from "./space"; +import { HttpRemoteSpace } from "./space"; import customMarkdownStyle from "./style"; import dbSyscalls from "./syscalls/db.localstorage"; import editorSyscalls from "./syscalls/editor.browser"; @@ -78,14 +78,14 @@ export class Editor implements AppEventDispatcher { viewState: AppViewState; viewDispatch: React.Dispatch; openPages: Map; - space: Space; + space: HttpRemoteSpace; editorCommands: Map; plugs: Plug[]; indexer: Indexer; navigationResolve?: (val: undefined) => void; pageNavigator: IPageNavigator; - constructor(space: Space, parent: Element) { + constructor(space: HttpRemoteSpace, parent: Element) { this.editorCommands = new Map(); this.openPages = new Map(); this.plugs = []; diff --git a/webapp/src/space.ts b/webapp/src/space.ts index deebf8d0..0e4572cf 100644 --- a/webapp/src/space.ts +++ b/webapp/src/space.ts @@ -1,4 +1,7 @@ import { PageMeta } from "./types"; +import { Socket } from "socket.io-client"; +import { serverEvents } from "../../server/src/events"; +import { EventEmitter } from "events"; export interface Space { listPages(): Promise; @@ -10,9 +13,15 @@ export interface Space { export class HttpRemoteSpace implements Space { url: string; + socket: Socket; - constructor(url: string) { + constructor(url: string, socket: Socket) { this.url = url; + this.socket = socket; + + socket.on("connect", () => { + console.log("connected via SocketIO", serverEvents.pageText); + }); } async listPages(): Promise { @@ -26,6 +35,13 @@ export class HttpRemoteSpace implements Space { })); } + async openPage(name: string) { + this.socket.on(serverEvents.pageText, (pageName, text) => { + console.log("Got this", pageName, text); + }); + this.socket.emit(serverEvents.openPage, "start"); + } + async readPage(name: string): Promise<{ text: string; meta: PageMeta }> { let req = await fetch(`${this.url}/${name}`, { method: "GET", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 3d119578..34138f6c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -947,6 +947,16 @@ chrome-trace-event "^1.0.2" nullthrows "^1.1.1" +"@socket.io/base64-arraybuffer@~1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" + integrity sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ== + +"@socket.io/component-emitter@~3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz#8863915676f837d9dad7b76f50cb500c1e9422e9" + integrity sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q== + "@swc/helpers@^0.2.11": version "0.2.14" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.2.14.tgz#20288c3627442339dd3d743c944f7043ee3590f0" @@ -957,6 +967,11 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/events@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + "@types/node@^17.0.21": version "17.0.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" @@ -1045,6 +1060,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +backo2@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + base-x@^3.0.8: version "3.0.9" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" @@ -1450,6 +1470,13 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== +debug@~4.3.1, debug@~4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -1542,6 +1569,28 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +engine.io-client@~6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.1.1.tgz#800d4b9db5487d169686729e5bd887afa78d36b0" + integrity sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g== + dependencies: + "@socket.io/component-emitter" "~3.0.0" + debug "~4.3.1" + engine.io-parser "~5.0.0" + has-cors "1.1.0" + parseqs "0.0.6" + parseuri "0.0.6" + ws "~8.2.3" + xmlhttprequest-ssl "~2.0.0" + yeast "0.1.2" + +engine.io-parser@~5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" + integrity sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg== + dependencies: + "@socket.io/base64-arraybuffer" "~1.0.2" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -1685,6 +1734,11 @@ has-bigints@^1.0.1: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2034,6 +2088,11 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + msgpackr-extract@^1.0.14: version "1.0.16" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.16.tgz#701c4f6e6f25c100ae84557092274e8fffeefe45" @@ -2187,6 +2246,16 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== + +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== + path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -2619,6 +2688,26 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +socket.io-client@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.4.1.tgz#b6aa9448149d09b8d0b2bbf3d2fac310631fdec9" + integrity sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ== + dependencies: + "@socket.io/component-emitter" "~3.0.0" + backo2 "~1.0.2" + debug "~4.3.2" + engine.io-client "~6.1.1" + parseuri "0.0.6" + socket.io-parser "~4.1.1" + +socket.io-parser@~4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.1.2.tgz#0a97d4fb8e67022158a568450a6e41887e42035e" + integrity sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog== + dependencies: + "@socket.io/component-emitter" "~3.0.0" + debug "~4.3.1" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -2837,6 +2926,16 @@ which-typed-array@^1.1.2: has-tostringtag "^1.0.0" is-typed-array "^1.1.7" +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xxhash-wasm@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79" @@ -2846,3 +2945,8 @@ yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=