Files
upstream-hocuspocus/tests/server/broadcastEncoding.ts
upstream-mirror e0c588ca1c
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s
snapshot of https://github.com/ueberdosis/hocuspocus.git @ cf3cfa355b4d3bb61c6b6c80071f71783e32470e (2026-09-15)
2026-09-15 17:21:35 +05:00

154 lines
5.1 KiB
TypeScript

import test from 'ava'
import { makeRoutingKey } from '@hocuspocus/common'
import { Document } from '@hocuspocus/server'
import * as decoding from 'lib0/decoding'
type FakeConnection = {
messageAddress: string
sent: Uint8Array[]
send: (message: Uint8Array) => void
}
/**
* Encoding is identical batched or not, so these tests opt out of batching to
* keep every assertion synchronous.
*/
const newDocument = () => new Document('hocuspocus-test', undefined, { flushDelay: false })
/**
* Registers a minimal stand-in for a Connection. The broadcast paths only read
* `messageAddress` and call `send`, so a duck-typed object is enough to observe
* what they produce.
*/
const addConnection = (document: Document, messageAddress: string): FakeConnection => {
const connection: FakeConnection = {
messageAddress,
sent: [],
send(message: Uint8Array) {
this.sent.push(message)
},
}
document.connections.set(connection as any, { clients: new Set() })
return connection
}
const readAddress = (message: Uint8Array) => decoding.readVarString(decoding.createDecoder(message))
test('encodes a document update once and shares it across connections', t => {
const document = newDocument()
const first = addConnection(document, 'hocuspocus-test')
const second = addConnection(document, 'hocuspocus-test')
const third = addConnection(document, 'hocuspocus-test')
document.getMap('test').set('foo', 'bar')
t.is(first.sent.length, 1)
t.is(second.sent.length, 1)
t.is(third.sent.length, 1)
// The same buffer instance reaches every connection, so it was encoded once.
t.is(first.sent[0], second.sent[0])
t.is(first.sent[0], third.sent[0])
})
test('encodes an awareness update once and shares it across connections', t => {
const document = newDocument()
const first = addConnection(document, 'hocuspocus-test')
const second = addConnection(document, 'hocuspocus-test')
document.awareness.setLocalState({ foo: 'bar' })
t.is(first.sent.length, 1)
t.is(second.sent.length, 1)
t.is(first.sent[0], second.sent[0])
})
test('encodes a stateless broadcast once and shares it across connections', t => {
const document = newDocument()
const first = addConnection(document, 'hocuspocus-test')
const second = addConnection(document, 'hocuspocus-test')
document.broadcastStateless('{"event":"document.saved"}')
t.is(first.sent.length, 1)
t.is(second.sent.length, 1)
t.is(first.sent[0], second.sent[0])
})
test('a filtered stateless broadcast only reaches matching connections', t => {
const document = newDocument()
const first = addConnection(document, 'hocuspocus-test')
const skipped = addConnection(document, 'hocuspocus-test')
const third = addConnection(document, 'hocuspocus-test')
document.broadcastStateless('{"event":"document.saved"}', connection => connection !== (skipped as any))
t.is(first.sent.length, 1)
t.is(skipped.sent.length, 0)
t.is(third.sent.length, 1)
// A skipped connection in the middle must not cost the others their shared buffer.
t.is(first.sent[0], third.sent[0])
})
test('addresses stateless broadcasts per connection', t => {
const document = newDocument()
const legacy = addConnection(document, 'hocuspocus-test')
const session = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a'))
document.broadcastStateless('{"event":"document.saved"}')
t.is(readAddress(legacy.sent[0]), 'hocuspocus-test')
t.is(readAddress(session.sent[0]), makeRoutingKey('hocuspocus-test', 'session-a'))
t.not(legacy.sent[0], session.sent[0])
})
test('reuses the buffer across consecutive connections sharing an address', t => {
const document = newDocument()
const legacy = addConnection(document, 'hocuspocus-test')
const sessionA = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a'))
const sessionAToo = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a'))
const sessionB = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-b'))
document.getMap('test').set('foo', 'bar')
t.is(sessionA.sent[0], sessionAToo.sent[0])
t.not(legacy.sent[0], sessionA.sent[0])
t.not(sessionA.sent[0], sessionB.sent[0])
const buffers = new Set([legacy.sent[0], sessionA.sent[0], sessionAToo.sent[0], sessionB.sent[0]])
t.is(buffers.size, 3)
})
test('addresses every broadcast message to the receiving connection', t => {
const document = newDocument()
// Interleaved addresses: buffer reuse does not apply here, so this also
// covers the fallback path where every connection gets its own encode.
const connections = [
addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')),
addConnection(document, 'hocuspocus-test'),
addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')),
addConnection(document, makeRoutingKey('hocuspocus-test', 'session-b')),
]
document.getMap('test').set('foo', 'bar')
document.awareness.setLocalState({ foo: 'bar' })
for (const connection of connections) {
t.is(connection.sent.length, 2)
for (const message of connection.sent) {
t.is(readAddress(message), connection.messageAddress)
}
}
})