diff --git a/.env.sample b/.env.sample index 874b2cef..14f7cedc 100644 --- a/.env.sample +++ b/.env.sample @@ -29,8 +29,8 @@ REDIS_URL=redis://localhost:6479 URL=http://localhost:3000 PORT=3000 -# ALPHA – See [documentation](docs/SERVICES.md) on running the alpha version of -# the collaboration server. +# See [documentation](docs/SERVICES.md) on running a separate collaboration +# server, for normal operation this does not need to be set. COLLABORATION_URL= # To support uploading of images for avatars and document attachments an diff --git a/app/components/Sidebar/Settings.js b/app/components/Sidebar/Settings.js index 903b39c9..8b7808c2 100644 --- a/app/components/Sidebar/Settings.js +++ b/app/components/Sidebar/Settings.js @@ -96,15 +96,13 @@ function SettingsSidebar() { label={t("Security")} /> )} - {can.update && - env.COLLABORATION_URL && - env.DEPLOYMENT !== "hosted" && ( - } - label={t("Features")} - /> - )} + {can.update && env.DEPLOYMENT !== "hosted" && ( + } + label={t("Features")} + /> + )} } diff --git a/app/scenes/Settings/Features.js b/app/scenes/Settings/Features.js index 80e04168..8a87249c 100644 --- a/app/scenes/Settings/Features.js +++ b/app/scenes/Settings/Features.js @@ -9,7 +9,6 @@ import Checkbox from "components/Checkbox"; import Heading from "components/Heading"; import HelpText from "components/HelpText"; import Scene from "components/Scene"; -import env from "env"; import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; import useToasts from "hooks/useToasts"; @@ -53,16 +52,13 @@ function Features() { all team members. - - {env.COLLABORATION_URL && ( - - )} + ); } diff --git a/docs/SERVICES.md b/docs/SERVICES.md index 67522d07..afc912e1 100644 --- a/docs/SERVICES.md +++ b/docs/SERVICES.md @@ -32,14 +32,13 @@ At least one worker process is required to process the [queues](../server/queues ## Collaboration -The service is in alpha and as such is not started by default. It must run -separately to the `websockets` service, and will not start in the same process. -The `COLLABORATION_URL` must be set to the publicly accessible URL when running -the service. For example, if the app is hosted at `https://docs.example.com` you -may use something like: `COLLABORATION_URL=wss://docs-collaboration.example.com`. - -Start the service with: +The service is in alpha and as such is not started by default. Start the service with: ```bash yarn start --services=collaboration ``` + +The collaboration service is ideally run on a separate server. If this is the +case the `COLLABORATION_URL` env can be set to the publicly accessible URL. For +example, if the app is hosted at `https://docs.example.com` you may use something +like: `COLLABORATION_URL=wss://docs-collaboration.example.com`. diff --git a/package.json b/package.json index f937838a..7e5fdbc6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:webpack": "webpack --config webpack.config.prod.js", "build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "start": "node ./build/server/index.js", - "dev": "yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=websockets,admin,web,worker\" \"node build/server/index.js --services=collaboration --port=4000\"", + "dev": "yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"", "dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/", "lint": "eslint app server shared", "deploy": "git push heroku master", @@ -101,7 +101,6 @@ "koa-body": "^4.2.0", "koa-compress": "2.0.0", "koa-convert": "1.2.0", - "koa-easy-ws": "^1.3.0", "koa-helmet": "5.2.0", "koa-jwt": "^3.6.0", "koa-logger": "^3.2.1", @@ -175,6 +174,7 @@ "uuid": "^8.3.2", "validator": "5.2.0", "winston": "^3.3.3", + "ws": "^7.5.3", "y-indexeddb": "^9.0.6", "y-prosemirror": "^1.0.9", "yjs": "^13.5.12" diff --git a/server/index.js b/server/index.js index b3e91503..8e7e0ce2 100644 --- a/server/index.js +++ b/server/index.js @@ -33,9 +33,17 @@ const serviceNames = uniq( // The number of processes to run, defaults to the number of CPU's available // for the web service, and 1 for collaboration during the beta period. -const processCount = serviceNames.includes("collaboration") - ? 1 - : env.WEB_CONCURRENCY || undefined; +let processCount = env.WEB_CONCURRENCY || undefined; + +if (serviceNames.includes("collaboration")) { + if (env.WEB_CONCURRENCY !== 1) { + Logger.info( + "lifecycle", + "Note: Restricting process count to 1 due to use of collaborative service" + ); + } + processCount = 1; +} // This function will only be called once in the original process function master() { @@ -72,15 +80,6 @@ async function start(id: string, disconnect: () => void) { router.get("/_health", (ctx) => (ctx.body = "OK")); app.use(router.routes()); - if ( - serviceNames.includes("websockets") && - serviceNames.includes("collaboration") - ) { - throw new Error( - "Cannot run websockets and collaboration services in the same process" - ); - } - // loop through requested services at startup for (const name of serviceNames) { if (!Object.keys(services).includes(name)) { diff --git a/server/presenters/env.js b/server/presenters/env.js index 4446d93c..adf16ac9 100644 --- a/server/presenters/env.js +++ b/server/presenters/env.js @@ -6,7 +6,7 @@ export default function present(env: Object): Object { return { URL: env.URL.replace(/\/$/, ""), CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""), - COLLABORATION_URL: (env.COLLABORATION_URL || "") + COLLABORATION_URL: (env.COLLABORATION_URL || env.URL) .replace(/\/$/, "") .replace(/^http/, "ws"), DEPLOYMENT: env.DEPLOYMENT, diff --git a/server/presenters/team.js b/server/presenters/team.js index f78e5443..e6feca75 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -1,5 +1,4 @@ // @flow -import env from "../env"; import { Team } from "../models"; export default function present(team: Team) { @@ -8,9 +7,7 @@ export default function present(team: Team) { name: team.name, avatarUrl: team.logoUrl, sharing: team.sharing, - collaborativeEditing: !!( - team.collaborativeEditing && env.COLLABORATION_URL - ), + collaborativeEditing: team.collaborativeEditing, documentEmbeds: team.documentEmbeds, guestSignin: team.guestSignin, subdomain: team.subdomain, diff --git a/server/services/collaboration.js b/server/services/collaboration.js index 2bf0a167..bd3773cf 100644 --- a/server/services/collaboration.js +++ b/server/services/collaboration.js @@ -1,16 +1,17 @@ // @flow import http from "http"; +import url from "url"; import { Server } from "@hocuspocus/server"; import Koa from "koa"; -import websocket from "koa-easy-ws"; -import Router from "koa-router"; +import WebSocket from "ws"; import AuthenticationExtension from "../collaboration/authentication"; import LoggerExtension from "../collaboration/logger"; import PersistenceExtension from "../collaboration/persistence"; import TracingExtension from "../collaboration/tracing"; export default function init(app: Koa, server: http.Server) { - const router = new Router(); + const path = "/collaboration"; + const wss = new WebSocket.Server({ noServer: true }); const hocuspocus = Server.configure({ extensions: [ @@ -21,22 +22,16 @@ export default function init(app: Koa, server: http.Server) { ], }); - // Websockets for collaborative editing - router.get("/collaboration/:documentName", async (ctx) => { - let { documentName } = ctx.params; + server.on("upgrade", function (req, socket, head) { + if (req.url.indexOf(path) > -1) { + const documentName = url.parse(req.url).pathname?.split("/").pop(); - if (ctx.ws) { - const ws = await ctx.ws(); - hocuspocus.handleConnection(ws, ctx.request, documentName); + wss.handleUpgrade(req, socket, Buffer.alloc(0), (client) => { + hocuspocus.handleConnection(client, req, documentName); + }); } - - ctx.response.status = 101; }); - app.use(websocket()); - app.use(router.routes()); - app.use(router.allowedMethods()); - server.on("shutdown", () => { hocuspocus.destroy(); }); diff --git a/server/services/websockets.js b/server/services/websockets.js index 7bb81318..36f770ed 100644 --- a/server/services/websockets.js +++ b/server/services/websockets.js @@ -16,13 +16,28 @@ import { getUserForJWT } from "../utils/jwt"; const { can } = policy; export default function init(app: Koa, server: http.Server) { + const path = "/realtime"; + // Websockets for events and non-collaborative documents const io = IO(server, { - path: "/realtime", + path, serveClient: false, cookie: false, }); + // Remove the upgrade handler that we just added when registering the IO engine + // And re-add it with a check to only handle the realtime path, this allows + // collaboration websockets to exist in the same process as engine.io. + const listeners = server.listeners("upgrade"); + const ioHandleUpgrade = listeners.pop(); + server.removeListener("upgrade", ioHandleUpgrade); + + server.on("upgrade", function (req, socket, head) { + if (req.url.indexOf(path) > -1) { + ioHandleUpgrade(req, socket, head); + } + }); + server.on("shutdown", () => { Metrics.gaugePerInstance("websockets.count", 0); }); diff --git a/yarn.lock b/yarn.lock index ae669158..77f0b6ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9261,14 +9261,6 @@ koa-convert@1.2.0, koa-convert@^1.2.0: co "^4.6.0" koa-compose "^3.0.0" -koa-easy-ws@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/koa-easy-ws/-/koa-easy-ws-1.3.0.tgz#dbf8eeb126ca11eed4c4e3af7904a47c25be2347" - integrity sha512-06lHAwm25HBdplTpwHiZqKcl39vS4KiVzRPneUExCHVvQ9TPHmFlUFO/gjQrUdLj3+kvqQpqdlIPNb91wn6EwA== - dependencies: - debug "^4.1.1" - ws "^7.3.1" - koa-helmet@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/koa-helmet/-/koa-helmet-5.2.0.tgz#6529f64dd4539261a9bb0a56e201e4976f0200f0" @@ -15496,10 +15488,10 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@^7.2.3, ws@^7.3.1, ws@^7.4.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +ws@^7.2.3, ws@^7.4.3, ws@^7.5.3: + version "7.5.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" + integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== ws@~7.4.2: version "7.4.6"