Skip to content

Commit a6a7bcd

Browse files
authored
feat: add beforeSync hook (#919)
1 parent d55ebf4 commit a6a7bcd

File tree

8 files changed

+258
-7
lines changed

8 files changed

+258
-7
lines changed

docs/server/hooks.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ By way of illustration, if a user isn’t allowed to connect: Just throw an erro
4949
| `beforeBroadcastStateless` | Before broadcast a stateless message | [Read more](/server/hooks#before-broadcast-stateless) |
5050
| `afterUnloadDocument` | When a document is closed | [Read more](/server/hooks#after-unload-document) |
5151

52-
5352
## Usage
5453

5554
```js
@@ -759,7 +758,6 @@ const server = new Server({
759758
server.listen();
760759
```
761760
762-
763761
### onStateless
764762
765763
The `onStateless` hooks are called after the server has received a stateless message. It should return a Promise.
@@ -796,6 +794,37 @@ const server = new Server({
796794
server.listen()
797795
```
798796
797+
### beforeSync
798+
799+
The `beforeSync` hooks are called before a sync message is handled. This is useful if you want to inspect the sync message that will be applied to the document.
800+
801+
**Hook payload**
802+
803+
```js
804+
const data = {
805+
documentName: string,
806+
document: Document,
807+
// The y-protocols/sync message type
808+
type: number,
809+
// The payload of the y-protocols/sync message
810+
payload: Uint8Array,
811+
}
812+
```
813+
814+
**Example**
815+
816+
```js
817+
import { Server } from '@hocuspocus/server'
818+
819+
const server = new Server({
820+
async beforeSync({ payload, document, documentName, type }) {
821+
console.log(`Server will handle a sync message: "${payload}"!`)
822+
},
823+
})
824+
825+
server.listen()
826+
```
827+
799828
### beforeBroadcastStateless
800829
801830
The `beforeBroadcastStateless` hooks are called before the server broadcast a stateless message.

packages/server/src/ClientConnection.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { OutgoingMessage } from './OutgoingMessage.ts'
1616
import type {
1717
ConnectionConfiguration,
1818
beforeHandleMessagePayload,
19+
beforeSyncPayload,
1920
onDisconnectPayload,
2021
} from './types.ts'
2122
import {
@@ -199,6 +200,20 @@ export class ClientConnection {
199200
return this.hooks('beforeHandleMessage', beforeHandleMessagePayload)
200201
})
201202

203+
instance.beforeSync((connection, payload) => {
204+
const beforeSyncPayload: beforeSyncPayload = {
205+
clientsCount: document.getConnectionsCount(),
206+
context: hookPayload.context,
207+
document,
208+
documentName: document.name,
209+
connection,
210+
type: payload.type,
211+
payload: payload.payload,
212+
}
213+
214+
return this.hooks('beforeSync', beforeSyncPayload)
215+
})
216+
202217
return instance
203218
}
204219

packages/server/src/Connection.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type Document from './Document.ts'
88
import { IncomingMessage } from './IncomingMessage.ts'
99
import { MessageReceiver } from './MessageReceiver.ts'
1010
import { OutgoingMessage } from './OutgoingMessage.ts'
11-
import type { onStatelessPayload } from './types.ts'
11+
import type { beforeSyncPayload, onStatelessPayload } from './types.ts'
1212

1313
export class Connection {
1414

@@ -23,6 +23,7 @@ export class Connection {
2323
callbacks = {
2424
onClose: [(document: Document, event?: CloseEvent) => {}],
2525
beforeHandleMessage: (connection: Connection, update: Uint8Array) => Promise.resolve(),
26+
beforeSync: (connection: Connection, payload: Pick<beforeSyncPayload, 'type' | 'payload'>) => Promise.resolve(),
2627
statelessCallback: (payload: onStatelessPayload) => Promise.resolve(),
2728
}
2829

@@ -81,6 +82,15 @@ export class Connection {
8182
return this
8283
}
8384

85+
/**
86+
* Set a callback that will be triggered before a sync message is handled
87+
*/
88+
beforeSync(callback: (connection: Connection, payload: Pick<beforeSyncPayload, 'type' | 'payload'>) => Promise<any>): Connection {
89+
this.callbacks.beforeSync = callback
90+
91+
return this
92+
}
93+
8494
/**
8595
* Send the given message
8696
*/

packages/server/src/Hocuspocus.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class Hocuspocus {
4949
onConnect: () => new Promise(r => r(null)),
5050
connected: () => new Promise(r => r(null)),
5151
beforeHandleMessage: () => new Promise(r => r(null)),
52+
beforeSync: () => new Promise(r => r(null)),
5253
beforeBroadcastStateless: () => new Promise(r => r(null)),
5354
onStateless: () => new Promise(r => r(null)),
5455
onChange: () => new Promise(r => r(null)),
@@ -111,6 +112,7 @@ export class Hocuspocus {
111112
afterLoadDocument: this.configuration.afterLoadDocument,
112113
beforeHandleMessage: this.configuration.beforeHandleMessage,
113114
beforeBroadcastStateless: this.configuration.beforeBroadcastStateless,
115+
beforeSync: this.configuration.beforeSync,
114116
onStateless: this.configuration.onStateless,
115117
onChange: this.configuration.onChange,
116118
onStoreDocument: this.configuration.onStoreDocument,

packages/server/src/IncomingMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export class IncomingMessage {
4949
return readVarUint8Array(this.decoder)
5050
}
5151

52+
peekVarUint8Array() {
53+
const { pos } = this.decoder
54+
const result = readVarUint8Array(this.decoder)
55+
this.decoder.pos = pos
56+
return result
57+
}
58+
5259
readVarUint() {
5360
return readVarUint(this.decoder)
5461
}

packages/server/src/MessageReceiver.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class MessageReceiver {
2727
this.defaultTransactionOrigin = defaultTransactionOrigin
2828
}
2929

30-
public apply(document: Document, connection?: Connection, reply?: (message: Uint8Array) => void) {
30+
public async apply(document: Document, connection?: Connection, reply?: (message: Uint8Array) => void) {
3131
const { message } = this
3232
const type = message.readVarUint()
3333
const emptyMessageLength = message.length
@@ -36,7 +36,7 @@ export class MessageReceiver {
3636
case MessageType.Sync:
3737
case MessageType.SyncReply: {
3838
message.writeVarUint(MessageType.Sync)
39-
this.readSyncMessage(message, document, connection, reply, type !== MessageType.SyncReply)
39+
await this.readSyncMessage(message, document, connection, reply, type !== MessageType.SyncReply)
4040

4141
if (message.length > emptyMessageLength + 1) {
4242
if (reply) {
@@ -55,7 +55,7 @@ export class MessageReceiver {
5555
break
5656
}
5757
case MessageType.Awareness: {
58-
applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection?.webSocket)
58+
await applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection?.webSocket)
5959

6060
break
6161
}
@@ -101,9 +101,16 @@ export class MessageReceiver {
101101
}
102102
}
103103

104-
readSyncMessage(message: IncomingMessage, document: Document, connection?: Connection, reply?: (message: Uint8Array) => void, requestFirstSync = true) {
104+
async readSyncMessage(message: IncomingMessage, document: Document, connection?: Connection, reply?: (message: Uint8Array) => void, requestFirstSync = true) {
105105
const type = message.readVarUint()
106106

107+
if (connection) {
108+
await connection.callbacks.beforeSync(connection, {
109+
type,
110+
payload: message.peekVarUint8Array(),
111+
})
112+
}
113+
107114
switch (type) {
108115
case messageYjsSyncStep1: {
109116
readSyncStep1(message.decoder, message.encoder, document)

packages/server/src/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface Extension {
4444
onLoadDocument?(data: onLoadDocumentPayload): Promise<any>;
4545
afterLoadDocument?(data: afterLoadDocumentPayload): Promise<any>;
4646
beforeHandleMessage?(data: beforeHandleMessagePayload): Promise<any>;
47+
beforeSync?(data: beforeSyncPayload): Promise<any>;
4748
beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise<any>;
4849
onStateless?(payload: onStatelessPayload): Promise<any>;
4950
onChange?(data: onChangePayload): Promise<any>;
@@ -69,6 +70,7 @@ export type HookName =
6970
'afterLoadDocument' |
7071
'beforeHandleMessage' |
7172
'beforeBroadcastStateless' |
73+
'beforeSync' |
7274
'onStateless' |
7375
'onChange' |
7476
'onStoreDocument' |
@@ -92,6 +94,7 @@ export type HookPayloadByName = {
9294
afterLoadDocument: afterLoadDocumentPayload,
9395
beforeHandleMessage: beforeHandleMessagePayload,
9496
beforeBroadcastStateless: beforeBroadcastStatelessPayload,
97+
beforeSync: beforeSyncPayload,
9598
onStateless: onStatelessPayload,
9699
onChange: onChangePayload,
97100
onStoreDocument: onStoreDocumentPayload,
@@ -250,6 +253,28 @@ export interface beforeHandleMessagePayload {
250253
connection: Connection
251254
}
252255

256+
export interface beforeSyncPayload {
257+
clientsCount: number,
258+
context: any,
259+
document: Document,
260+
documentName: string,
261+
connection: Connection,
262+
/**
263+
* The y-protocols/sync message type
264+
* @example
265+
* 0: SyncStep1
266+
* 1: SyncStep2
267+
* 2: YjsUpdate
268+
*
269+
* @see https://github.com/yjs/y-protocols/blob/master/sync.js#L13-L40
270+
*/
271+
type: number,
272+
/**
273+
* The payload of the y-sync message.
274+
*/
275+
payload: Uint8Array,
276+
}
277+
253278
export interface beforeBroadcastStatelessPayload {
254279
document: Document,
255280
documentName: string,

0 commit comments

Comments
 (0)