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
161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
import test from 'ava'
|
|
import * as encoding from 'lib0/encoding'
|
|
import { HocuspocusProviderWebsocket } from '@hocuspocus/provider'
|
|
import { newHocuspocus } from '../utils/index.ts'
|
|
|
|
/**
|
|
* Helper to create a raw message Uint8Array with a given documentName and messageType,
|
|
* matching the wire format: VarString(documentName) + VarUint(messageType) + optional payload.
|
|
*/
|
|
function createRawMessage(documentName: string, messageType: number, payload?: Uint8Array): Uint8Array {
|
|
const encoder = encoding.createEncoder()
|
|
encoding.writeVarString(encoder, documentName)
|
|
encoding.writeVarUint(encoder, messageType)
|
|
if (payload) {
|
|
encoding.writeVarUint8Array(encoder, payload)
|
|
}
|
|
return encoding.toUint8Array(encoder)
|
|
}
|
|
|
|
// MessageType enum values (matching provider/src/types.ts)
|
|
const MessageType = {
|
|
Sync: 0,
|
|
Awareness: 1,
|
|
Auth: 2,
|
|
QueryAwareness: 3,
|
|
Stateless: 5,
|
|
CLOSE: 7,
|
|
}
|
|
|
|
test('deduplicates awareness messages for the same document in the queue', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
// Queue multiple awareness messages for the same document
|
|
const msg1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([1]))
|
|
const msg2 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([2]))
|
|
const msg3 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([3]))
|
|
|
|
ws.send(msg1)
|
|
ws.send(msg2)
|
|
ws.send(msg3)
|
|
|
|
// Access the private messageQueue via cast
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 1, 'should only have the latest awareness message')
|
|
t.deepEqual(queue[0], msg3, 'should keep the last queued awareness message')
|
|
})
|
|
|
|
test('deduplicates QueryAwareness messages for the same document in the queue', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
const msg1 = createRawMessage('doc1', MessageType.QueryAwareness)
|
|
const msg2 = createRawMessage('doc1', MessageType.QueryAwareness)
|
|
|
|
ws.send(msg1)
|
|
ws.send(msg2)
|
|
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 1, 'should only have the latest QueryAwareness message')
|
|
t.deepEqual(queue[0], msg2)
|
|
})
|
|
|
|
test('does not deduplicate awareness messages for different documents', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
const msg1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([1]))
|
|
const msg2 = createRawMessage('doc2', MessageType.Awareness, new Uint8Array([2]))
|
|
|
|
ws.send(msg1)
|
|
ws.send(msg2)
|
|
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 2, 'should keep awareness messages for different documents')
|
|
})
|
|
|
|
test('does not deduplicate Sync messages', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
const msg1 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([1]))
|
|
const msg2 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([2]))
|
|
const msg3 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([3]))
|
|
|
|
ws.send(msg1)
|
|
ws.send(msg2)
|
|
ws.send(msg3)
|
|
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 3, 'should keep all Sync messages')
|
|
})
|
|
|
|
test('does not deduplicate Stateless messages', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
const msg1 = createRawMessage('doc1', MessageType.Stateless, new Uint8Array([1]))
|
|
const msg2 = createRawMessage('doc1', MessageType.Stateless, new Uint8Array([2]))
|
|
|
|
ws.send(msg1)
|
|
ws.send(msg2)
|
|
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 2, 'should keep all Stateless messages')
|
|
})
|
|
|
|
test('deduplicates awareness but preserves sync messages in mixed queue', async t => {
|
|
const server = await newHocuspocus(t)
|
|
|
|
const ws = new HocuspocusProviderWebsocket({
|
|
url: server.server!.webSocketURL,
|
|
autoConnect: false,
|
|
})
|
|
t.teardown(() => ws.destroy())
|
|
|
|
const sync1 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([1]))
|
|
const awareness1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([10]))
|
|
const sync2 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([2]))
|
|
const awareness2 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([20]))
|
|
const sync3 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([3]))
|
|
|
|
ws.send(sync1)
|
|
ws.send(awareness1)
|
|
ws.send(sync2)
|
|
ws.send(awareness2)
|
|
ws.send(sync3)
|
|
|
|
const queue = (ws as any).messageQueue as Uint8Array[]
|
|
t.is(queue.length, 4, 'should have 3 sync + 1 awareness')
|
|
t.deepEqual(queue[0], sync1)
|
|
t.deepEqual(queue[1], sync2)
|
|
t.deepEqual(queue[2], awareness2, 'should keep the latest awareness message')
|
|
t.deepEqual(queue[3], sync3)
|
|
})
|