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"