Skip to content

Commit 2666542

Browse files
authored
feat: cross-ws (bun, deno, cloudflare, node uwebsocket) (#1056)
* wip * wip * wip * feat: replace listeners by crossws-way * chore: adds bun types * bun fix * type fix in bun.ts * pnpm lock * fix: cleanup * provider: handle unknown messages gracefully (log instead of throw) * replace ping/pong by timeout based on message received. awareness is sent every 30 seconds, so we can use that instead of ping/pong. ping/pong can't be added to the protocol because old provider would throw. * fix: make sure that all dist files keep node: prefixes. Deno didnt work because "url" was required without node: prefix ; this is actually coming from the "ws" package, not from our code base... The fix works by checking all require/imports, then checking if they are present in node and then prefixes them
1 parent 81ce838 commit 2666542

File tree

28 files changed

+421
-324
lines changed

28 files changed

+421
-324
lines changed

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
"browserslist": ["defaults", "not IE 11", "maintained node versions"],
55
"type": "module",
66
"ava": {
7-
"files": ["tests/**/*", "!tests/utils/**/*", "!database/**/*"],
7+
"files": [
8+
"tests/**/*",
9+
"!tests/utils/**/*",
10+
"!database/**/*"
11+
],
812
"extensions": {
913
"ts": "module"
1014
},
@@ -49,6 +53,7 @@
4953
"devDependencies": {
5054
"@ava/typescript": "^3.0.1",
5155
"@biomejs/biome": "1.9.4",
56+
"@types/bun": "^1.3.9",
5257
"ava": "^4.3.3",
5358
"concurrently": "^6.4.0",
5459
"lerna": "^9.0.3",

packages/extension-redis/src/Redis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export class Redis implements Extension {
142142
this.pub = new RedisClient(port, host, options ?? {});
143143
this.sub = new RedisClient(port, host, options ?? {});
144144
}
145+
145146
this.sub.on("messageBuffer", this.handleIncomingMessage);
146147

147148
this.redlock = new Redlock([this.pub], {

packages/extension-throttle/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,10 @@ export class Throttle implements Extension {
114114
onConnect(data: onConnectPayload): Promise<any> {
115115
const { request } = data;
116116

117-
// get the remote ip address
117+
// get the remote ip address from headers (web standard Request doesn't have socket)
118118
const ip =
119-
request.headers["x-real-ip"] ||
120-
request.headers["x-forwarded-for"] ||
121-
request.socket.remoteAddress ||
119+
request.headers.get("x-real-ip") ||
120+
request.headers.get("x-forwarded-for") ||
122121
"";
123122

124123
// throttle the connection

packages/provider/src/HocuspocusProvider.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { awarenessStatesToArray } from "@hocuspocus/common";
2-
import type { Event, MessageEvent } from "ws";
32
import { Awareness, removeAwarenessStates } from "y-protocols/awareness";
43
import * as Y from "yjs";
54
import EventEmitter from "./EventEmitter.ts";
@@ -13,22 +12,22 @@ import { AwarenessMessage } from "./OutgoingMessages/AwarenessMessage.ts";
1312
import { StatelessMessage } from "./OutgoingMessages/StatelessMessage.ts";
1413
import { SyncStepOneMessage } from "./OutgoingMessages/SyncStepOneMessage.ts";
1514
import { UpdateMessage } from "./OutgoingMessages/UpdateMessage.ts";
16-
import type {
17-
AuthorizedScope,
18-
ConstructableOutgoingMessage,
19-
onAuthenticatedParameters,
20-
onAuthenticationFailedParameters,
21-
onAwarenessChangeParameters,
22-
onAwarenessUpdateParameters,
23-
onCloseParameters,
24-
onDisconnectParameters,
25-
onMessageParameters,
26-
onOpenParameters,
27-
onOutgoingMessageParameters,
28-
onStatelessParameters,
29-
onStatusParameters,
30-
onSyncedParameters,
31-
onUnsyncedChangesParameters,
15+
import {
16+
type AuthorizedScope,
17+
type ConstructableOutgoingMessage,
18+
type onAuthenticatedParameters,
19+
type onAuthenticationFailedParameters,
20+
type onAwarenessChangeParameters,
21+
type onAwarenessUpdateParameters,
22+
type onCloseParameters,
23+
type onDisconnectParameters,
24+
type onMessageParameters,
25+
type onOpenParameters,
26+
type onOutgoingMessageParameters,
27+
type onStatelessParameters,
28+
type onStatusParameters,
29+
type onSyncedParameters,
30+
type onUnsyncedChangesParameters,
3231
} from "./types.ts";
3332

3433
export type HocuspocusProviderConfiguration = Required<

packages/provider/src/HocuspocusProviderWebsocket.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { WsReadyStates } from "@hocuspocus/common";
22
import { retry } from "@lifeomic/attempt";
3+
import * as encoding from "lib0/encoding";
34
import * as time from "lib0/time";
4-
import type { Event, MessageEvent } from "ws";
55
import EventEmitter from "./EventEmitter.ts";
66
import type { HocuspocusProvider } from "./HocuspocusProvider.ts";
77
import { IncomingMessage } from "./IncomingMessage.ts";
88
import { CloseMessage } from "./OutgoingMessages/CloseMessage.ts";
99
import {
10+
MessageType,
1011
WebSocketStatus,
1112
type onAwarenessChangeParameters,
1213
type onAwarenessUpdateParameters,
@@ -373,12 +374,32 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
373374

374375
this.lastMessageReceived = time.getUnixTime();
375376

376-
const message = new IncomingMessage(event.data);
377+
const data = new Uint8Array(event.data as ArrayBuffer);
378+
379+
// Check for connection-level Ping message (no document name prefix)
380+
// Ping messages are sent as just the message type byte (length 1)
381+
// We check length to avoid confusing with regular messages that happen to have
382+
// a document name length of 9 as the first byte
383+
if (data.length === 1 && data[0] === MessageType.Ping) {
384+
this.sendPong();
385+
return;
386+
}
387+
388+
const message = new IncomingMessage(data);
377389
const documentName = message.peekVarString();
378390

379391
this.configuration.providerMap.get(documentName)?.onMessage(event);
380392
}
381393

394+
/**
395+
* Send application-level Pong response to server Ping
396+
*/
397+
private sendPong() {
398+
const encoder = encoding.createEncoder();
399+
encoding.writeVarUint(encoder, MessageType.Pong);
400+
this.send(encoding.toUint8Array(encoder));
401+
}
402+
382403
resolveConnectionAttempt() {
383404
if (this.connectionAttempt) {
384405
this.connectionAttempt.resolve();

packages/provider/src/MessageReceiver.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { readAuthMessage } from "@hocuspocus/common";
1+
import { type CloseEvent, readAuthMessage } from "@hocuspocus/common";
22
import { readVarInt, readVarString } from "lib0/decoding";
3-
import type { CloseEvent } from "ws";
43
import * as awarenessProtocol from "y-protocols/awareness";
54
import { messageYjsSyncStep2, readSyncMessage } from "y-protocols/sync";
65
import type { HocuspocusProvider } from "./HocuspocusProvider.ts";
@@ -54,17 +53,14 @@ export class MessageReceiver {
5453
const event: CloseEvent = {
5554
code: 1000,
5655
reason: readVarString(message.decoder),
57-
// @ts-ignore
58-
target: provider.configuration.websocketProvider.webSocket!,
59-
type: "close",
6056
};
6157
provider.onClose();
6258
provider.configuration.onClose({ event });
6359
provider.forwardClose({ event });
6460
break;
6561

6662
default:
67-
throw new Error(`Can’t apply message of unknown type: ${type}`);
63+
console.error(`Can’t apply message of unknown type: ${type}`);
6864
}
6965

7066
// Reply

packages/provider/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Encoder } from "lib0/encoding";
2-
import type { Event, MessageEvent } from "ws";
32
import type { Awareness } from "y-protocols/awareness";
43
import type * as Y from "yjs";
54
import type { CloseEvent } from "@hocuspocus/common";
@@ -20,6 +19,8 @@ export enum MessageType {
2019
Stateless = 5,
2120
CLOSE = 7,
2221
SyncStatus = 8,
22+
Ping = 9,
23+
Pong = 10,
2324
}
2425

2526
export enum WebSocketStatus {

packages/server/package.json

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
"description": "plug & play collaboration backend",
44
"version": "3.4.6-rc.2",
55
"homepage": "https://hocuspocus.dev",
6-
"keywords": [
7-
"hocuspocus",
8-
"yjs",
9-
"yjs-websocket",
10-
"prosemirror"
11-
],
6+
"keywords": ["hocuspocus", "yjs", "yjs-websocket", "prosemirror"],
127
"license": "MIT",
138
"type": "module",
149
"main": "dist/hocuspocus-server.cjs",
@@ -24,21 +19,17 @@
2419
"types": "./dist/index.d.ts"
2520
}
2621
},
27-
"files": [
28-
"src",
29-
"dist"
30-
],
22+
"files": ["src", "dist"],
3123
"dependencies": {
3224
"@hocuspocus/common": "workspace:^",
3325
"async-lock": "^1.3.1",
3426
"async-mutex": "^0.5.0",
27+
"crossws": "^0.3.4",
3528
"kleur": "^4.1.4",
36-
"lib0": "^0.2.47",
37-
"ws": "^8.5.0"
29+
"lib0": "^0.2.47"
3830
},
3931
"devDependencies": {
40-
"@types/async-lock": "^1.1.3",
41-
"@types/ws": "^8.5.3"
32+
"@types/async-lock": "^1.1.3"
4233
},
4334
"peerDependencies": {
4435
"y-protocols": "^1.0.6",
@@ -47,5 +38,8 @@
4738
"gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d",
4839
"repository": {
4940
"url": "https://github.com/ueberdosis/hocuspocus"
41+
},
42+
"engines": {
43+
"node": ">=22"
5044
}
5145
}

0 commit comments

Comments
 (0)