Skip to content

Commit ab32e17

Browse files
committed
feat: setup project to use hocuspocus v3
1 parent f4bf873 commit ab32e17

File tree

6 files changed

+74
-95
lines changed

6 files changed

+74
-95
lines changed

hocuspocus-server/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hocuspocus-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"devDependencies": {
3939
"@typescript-eslint/eslint-plugin": "^5.5.0",
4040
"@typescript-eslint/parser": "^5.5.0",
41+
"@types/node": "^20.11.18",
4142
"eslint": "^8.10.0",
4243
"eslint-config-react-app": "^7.0.0",
4344
"eslint-plugin-import": "^2.31.0",

hocuspocus-server/src/index.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SQLite } from "@hocuspocus/extension-sqlite";
2-
import { Document, Server } from "@hocuspocus/server";
2+
import { type Document, Hocuspocus } from "@hocuspocus/server";
33

44
import { serve } from "@hono/node-server";
55
import { createNodeWebSocket } from "@hono/node-ws";
@@ -10,7 +10,8 @@ import { FAKE_authInfoFromToken } from "./auth.js";
1010
import { threadsRouter } from "./threads.js";
1111
import { RejectUnauthorized } from "./rejectUnauthorized.js";
1212
// Setup Hocuspocus server
13-
const hocuspocusServer = Server.configure({
13+
14+
const hocuspocus = new Hocuspocus({
1415
async onAuthenticate(data) {
1516
const { token } = data;
1617

@@ -19,15 +20,19 @@ const hocuspocusServer = Server.configure({
1920
if (authInfo === "unauthorized") {
2021
throw new Error("Not authorized!");
2122
}
22-
23-
data.connection.readOnly = authInfo.role === "COMMENT-ONLY";
23+
24+
data.connectionConfig.readOnly = authInfo.role === "COMMENT-ONLY";
2425
},
2526

2627
extensions: [
2728
new SQLite({
28-
database: "db.sqlite",
29+
database: ":memory:",
30+
}),
31+
// TODO we can actually just do the auth check in here, and not need the server to inject the mark or anything
32+
new RejectUnauthorized("threads", (payload) => {
33+
// eslint-disable-next-line no-console
34+
console.warn("rejecting update to document", payload.documentName);
2935
}),
30-
new RejectUnauthorized("threads"),
3136
],
3237

3338
// TODO: for good security, you'd want to make sure that either:
@@ -46,7 +51,7 @@ app.get(
4651
"/hocuspocus",
4752
upgradeWebSocket((c) => ({
4853
onOpen(_evt, ws) {
49-
hocuspocusServer.handleConnection(ws.raw, c.req.raw);
54+
hocuspocus.handleConnection(ws.raw, c.req.raw as any);
5055
},
5156
}))
5257
);
@@ -61,7 +66,7 @@ const documentMiddleware = createMiddleware<{
6166
};
6267
}>(async (c, next) => {
6368
const documentId = c.req.param("documentId");
64-
const document = hocuspocusServer.documents.get(documentId!);
69+
const document = hocuspocus.documents.get(documentId!);
6570

6671
if (!document) {
6772
return c.json({ error: "Document not found" }, 404);
@@ -85,6 +90,12 @@ app.route(
8590
const server = serve({
8691
fetch: app.fetch,
8792
port: 8787,
93+
}, (info) => {
94+
hocuspocus.hooks('onListen', {
95+
instance: hocuspocus,
96+
configuration: hocuspocus.configuration,
97+
port: info.port
98+
})
8899
});
89100

90101
// Setup WebSocket support (needed for HocusPocus)
Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import type { CloseEvent } from "@hocuspocus/common";
21
import {
3-
beforeHandleMessagePayload,
4-
Extension,
5-
IncomingMessage,
6-
MessageType,
2+
type beforeSyncPayload,
3+
Extension
74
} from "@hocuspocus/server";
85
import * as syncProtocol from "y-protocols/sync";
96
import * as Y from "yjs";
@@ -18,55 +15,7 @@ import * as Y from "yjs";
1815
* - if the update is accepted, we do nothing
1916
*/
2017
export class RejectUnauthorized implements Extension {
21-
constructor(private readonly threadsMapKey: string) {}
22-
/**
23-
* Extract the yjsUpdate from the incoming message
24-
* @param message
25-
* @returns
26-
*/
27-
private getYUpdate(message: Uint8Array) {
28-
/**
29-
* The messages we are interested in are of the following format:
30-
* [docIdLength: number, ...docIdString: string, hocuspocusMessageType: number, ySyncMessageType: number, ...yjsUpdate: Uint8Array]
31-
*
32-
* We check that the hocuspocusMessageType is Sync and that the ySyncMessageType is messageYjsUpdate.
33-
*/
34-
const incomingMessage = new IncomingMessage(message);
35-
// Read the docID string, but don't use it
36-
incomingMessage.readVarString();
37-
38-
// Read the hocuspocusMessageType
39-
const hocuspocusMessageType = incomingMessage.readVarUint();
40-
// If the hocuspocusMessageType is not Sync, we don't handle the message, since it is not an update
41-
if (
42-
!(
43-
hocuspocusMessageType === MessageType.Sync ||
44-
hocuspocusMessageType === MessageType.SyncReply
45-
)
46-
) {
47-
return;
48-
}
49-
50-
// Read the ySyncMessageType
51-
const ySyncMessageType = incomingMessage.readVarUint();
52-
53-
// If the ySyncMessageType is not a messageYjsUpdate or a messageYjsSyncStep2, we don't handle the message, since it is not an update
54-
if (
55-
!(
56-
ySyncMessageType === syncProtocol.messageYjsUpdate ||
57-
ySyncMessageType === syncProtocol.messageYjsSyncStep2
58-
)
59-
) {
60-
// not an update we want to handle
61-
return;
62-
}
63-
64-
// Read the yjsUpdate
65-
const yUpdate = incomingMessage.readVarUint8Array();
66-
67-
return yUpdate;
68-
}
69-
18+
constructor(private readonly threadsMapKey: string, private readonly onReject?: (payload: beforeSyncPayload) => void) {}
7019
/**
7120
* This function protects against changes to the restricted type.
7221
* It does this by:
@@ -112,29 +61,31 @@ export class RejectUnauthorized implements Extension {
11261
return didNeedToUndo;
11362
}
11463

115-
async beforeHandleMessage({
116-
update,
117-
document: ydoc,
118-
}: beforeHandleMessagePayload) {
119-
const yUpdate = this.getYUpdate(update);
120-
121-
if (!yUpdate) {
64+
/**
65+
* Before the document is synchronized, we check if the update modifies the restricted type.
66+
* If it does, we reject the update by undoing it, and calling the onReject callback.
67+
*/
68+
async beforeSync(data: beforeSyncPayload) {
69+
// If the ySyncMessageType is not a messageYjsUpdate or a messageYjsSyncStep2, we don't handle the message, since it is not an update
70+
if (
71+
!(
72+
data.type === syncProtocol.messageYjsUpdate ||
73+
data.type === syncProtocol.messageYjsSyncStep2
74+
)
75+
) {
76+
// not an update we want to handle
12277
return;
12378
}
12479

125-
const protectedType = ydoc.getMap(this.threadsMapKey);
80+
const protectedType = data.document.getMap(this.threadsMapKey);
12681
const didRollback = this.applyUpdateAndRollbackIfNeeded(
127-
yUpdate,
128-
ydoc,
82+
data.payload,
83+
data.document,
12984
protectedType
13085
);
13186

13287
if (didRollback) {
133-
// TODO, we can close their connection or just let them continue, since we've already undone their changes (and our changes are newer than theirs)
134-
const error = {
135-
reason: `Modification of a restricted type: ${this.threadsMapKey} was rejected`,
136-
} satisfies Partial<CloseEvent>;
137-
throw error;
88+
this.onReject?.(data)
13889
}
13990
}
14091
}

next-app/package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

next-app/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
},
1111
"dependencies": {
1212
"@hocuspocus/provider": "^2.15.2",
13-
"@blocknote/core": "^0.25.1",
14-
"@blocknote/mantine": "^0.25.1",
15-
"@blocknote/react": "^0.25.1",
13+
"@blocknote/core": "^0.26.0",
14+
"@blocknote/mantine": "^0.26.0",
15+
"@blocknote/react": "^0.26.0",
1616
"next": "15.1.7",
1717
"react": "^19.0.0",
1818
"react-dom": "^19.0.0"

0 commit comments

Comments
 (0)