Skip to content

Commit

Permalink
refactor: extract common functions and types (TexteaInc#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
exuanbo committed Sep 5, 2022
1 parent b101e60 commit 282a1b4
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 40 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ module.exports = {
next: {
rootDir: 'app'
}
},
rules: {
'no-void': ['error', { allowAsStatement: true }]
}
}
10 changes: 5 additions & 5 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ const yDoc = new Y.Doc()
const yText = yDoc.getText()
const roomId = 'test-id'

const awareness = new Awareness(yDoc)

type User = {
id: number
id: Awareness['clientID']
name: string
}

const DEFAULT_USER: Readonly<User> = {
id: yDoc.clientID,
name: `ID_${yDoc.clientID.toString(16).toUpperCase()}`
id: awareness.clientID,
name: `ID_${awareness.clientID.toString(16).toUpperCase()}`
}

const awareness = new Awareness(yDoc)
awareness.setLocalState(DEFAULT_USER)

export const App: React.FC = () => {
Expand Down
12 changes: 12 additions & 0 deletions src/awareness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Awareness } from 'y-protocols/awareness'

import type { ClientId } from './types'

export type AwarenessChanges = Record<'added' | 'updated' | 'removed', ClientId[]>

export const getClients = (awareness: Awareness): ClientId[] => [...awareness.getStates().keys()]

export const getOtherClients = (awareness: Awareness): ClientId[] => {
const clients = getClients(awareness)
return clients.filter((clientId) => clientId !== awareness.clientID)
}
21 changes: 11 additions & 10 deletions src/events.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ClientId, RoomName } from './types'

type EventNameWithScope<Scope extends string, Type extends string = string> = `${Scope}:${Type}`

type YDocScope = 'doc'
Expand All @@ -20,25 +22,24 @@ type ValidateEvents<
> = Events

export type ServerToClientEvents = ValidateEvents<{
'doc:diff': (diff: ArrayBuffer) => void
'doc:update': (updateV2: ArrayBuffer) => void
'awareness:update': (update: ArrayBuffer) => void
['doc:diff']: (diff: ArrayBuffer) => void
['doc:update']: (updateV2: ArrayBuffer) => void
['awareness:update']: (update: ArrayBuffer) => void
}>

export type ClientToServerEvents = ValidateEvents<{
join: (roomName: string) => void

'doc:diff': (roomName: string, diff: Uint8Array) => void
'doc:update': (roomName: string, updateV2: Uint8Array, callback?: () => void) => void
'awareness:update': (roomName: string, update: Uint8Array) => void
join: (roomName: RoomName) => void
['doc:diff']: (roomName: RoomName, diff: Uint8Array) => void
['doc:update']: (roomName: RoomName, updateV2: Uint8Array, callback?: () => void) => void
['awareness:update']: (roomName: RoomName, update: Uint8Array) => void
}>

type ClientToServerEventNames = keyof ClientToServerEvents

export type BroadcastChannelMessageData<EventName extends ClientToServerEventNames = ClientToServerEventNames> =
| EventName extends ObservableEventName
? [eventName: EventName, payload: Uint8Array, clientId?: number]
? [eventName: EventName, payload: Uint8Array, clientId?: ClientId]
: never
| [eventName: `${AwarenessScope}:query`, clientId: number]
| [eventName: `${AwarenessScope}:query`, clientId: ClientId]

export type BroadcastChannelMessageEvent = MessageEvent<BroadcastChannelMessageData>
6 changes: 4 additions & 2 deletions src/persistence.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type * as Y from 'yjs'

import type { RoomName } from './types'

export interface Persistence {
bindState: (roomName: string, doc: Y.Doc) => Promise<void>
writeState: (roomName: string, doc: Y.Doc) => Promise<void>
bindState: (roomName: RoomName, doc: Y.Doc) => Promise<void>
writeState: (roomName: RoomName, doc: Y.Doc) => Promise<void>
}
29 changes: 14 additions & 15 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import * as Y from 'yjs'
import { createStore, Mutate, StoreApi } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

import { AwarenessChanges, getClients, getOtherClients } from './awareness'
import type {
BroadcastChannelMessageData,
BroadcastChannelMessageEvent,
ClientToServerEvents,
ServerToClientEvents
} from './events'
import type { AwarenessChanges } from './types'
import type { RoomName } from './types'

export interface Options {
awareness?: Awareness
Expand Down Expand Up @@ -57,7 +58,7 @@ export interface SocketIOProvider extends SocketIOProviderStore {

type CreateSocketIOProvider = (
serverUrl: string,
roomName: string,
roomName: RoomName,
doc: Y.Doc,
options?: Options
) => SocketIOProvider
Expand Down Expand Up @@ -102,7 +103,7 @@ export const createSocketIOProvider: CreateSocketIOProvider = (
}
})
if (awareness.getLocalState() !== null) {
const awarenessUpdate = encodeAwarenessUpdate(awareness, [doc.clientID])
const awarenessUpdate = encodeAwarenessUpdate(awareness, [awareness.clientID])
socket.emit('awareness:update', roomName, awarenessUpdate)
}
store.setState({
Expand All @@ -122,15 +123,13 @@ export const createSocketIOProvider: CreateSocketIOProvider = (
applyAwarenessUpdate(awareness, new Uint8Array(update), socket)
})
socket.on('disconnect', (_reason, description) => {
const err = description instanceof Error ? description : null
const otherClients = getOtherClients(awareness)
removeAwarenessStates(awareness, otherClients, socket)
syncingDocUpdates.clear()
const clients = [...awareness.getStates().keys()].filter(
(clientId) => clientId !== doc.clientID
)
removeAwarenessStates(awareness, clients, socket)
const err = description instanceof Error ? description.message : null
store.setState({
...INITIAL_STATE,
error: err
error: err?.message
})
})

Expand All @@ -152,21 +151,21 @@ export const createSocketIOProvider: CreateSocketIOProvider = (
}
case 'doc:update': {
const [, updateV2, clientId] = event.data
if (!clientId || clientId === doc.clientID) {
if (!clientId || clientId === awareness.clientID) {
Y.applyUpdateV2(doc, updateV2, socket)
}
break
}
case 'awareness:query': {
const [, clientId] = event.data
const clients = [...awareness.getStates().keys()]
const clients = getClients(awareness)
const update = encodeAwarenessUpdate(awareness, clients)
broadcastChannel!.postMessage(['awareness:update', update, clientId])
break
}
case 'awareness:update': {
const [, update, clientId] = event.data
if (!clientId || clientId === doc.clientID) {
if (!clientId || clientId === awareness.clientID) {
applyAwarenessUpdate(awareness, update, socket)
}
break
Expand All @@ -180,12 +179,12 @@ export const createSocketIOProvider: CreateSocketIOProvider = (
broadcastChannel = new BroadcastChannel(broadcastChannelName)
broadcastChannel.onmessage = handleBroadcastChannelMessage
const docDiff = Y.encodeStateVector(doc)
broadcastChannel.postMessage(['doc:diff', docDiff, doc.clientID])
broadcastChannel.postMessage(['doc:diff', docDiff, awareness.clientID])
const docUpdateV2 = Y.encodeStateAsUpdateV2(doc)
broadcastChannel.postMessage(['doc:update', docUpdateV2])
broadcastChannel.postMessage(['awareness:query', doc.clientID])
broadcastChannel.postMessage(['awareness:query', awareness.clientID])
if (awareness.getLocalState() !== null) {
const awarenessUpdate = encodeAwarenessUpdate(awareness, [doc.clientID])
const awarenessUpdate = encodeAwarenessUpdate(awareness, [awareness.clientID])
broadcastChannel.postMessage(['awareness:update', awarenessUpdate])
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/server/socket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { Server, Socket } from 'socket.io'
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate } from 'y-protocols/awareness'
import * as Y from 'yjs'

import type { AwarenessChanges } from '../../awareness'
import { getClients } from '../../awareness'
import type { ClientToServerEvents, ServerToClientEvents } from '../../events'
import type { Persistence } from '../../persistence'
import type { AwarenessChanges } from '../../types'
import type { RoomName } from '../../types'
import { createRoomMap, Room } from './room'

/**
Expand All @@ -30,7 +32,7 @@ export const createSocketServer = (httpServer: HTTPServer, persistence?: Persist

const { adapter } = io.of('/')

adapter.on('create-room', (roomName: string) => {
adapter.on('create-room', (roomName: RoomName) => {
// socket default room
if (adapter.sids.has(roomName)) {
return
Expand All @@ -54,19 +56,19 @@ export const createSocketServer = (httpServer: HTTPServer, persistence?: Persist
}
roomMap.set(roomName, createRoom())
})
adapter.on('delete-room', (roomName: string) => {
adapter.on('delete-room', (roomName: RoomName) => {
// socket default room
if (adapter.sids.has(roomName)) {
return
}
const loadingRoom = roomMap.get(roomName)!
const destroyRoom = async () => {
const destroyRoom = async (): Promise<void> => {
const room = await loadingRoom
await persistence?.writeState(roomName, room.doc)
room.doc.destroy()
room.awareness.destroy()
}
destroyRoom()
void destroyRoom()
roomMap.delete(roomName)
})

Expand All @@ -78,7 +80,7 @@ export const createSocketServer = (httpServer: HTTPServer, persistence?: Persist
socket.emit('doc:diff', docDiff)
const awarenessStates = room.awareness.getStates()
if (awarenessStates.size) {
const clients = [...awarenessStates.keys()]
const clients = getClients(room.awareness)
const awarenessUpdate = encodeAwarenessUpdate(room.awareness, clients)
socket.emit('awareness:update', awarenessUpdate)
}
Expand Down
3 changes: 2 additions & 1 deletion src/server/socket/room.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Room as RoomName } from 'socket.io-adapter'
import type { Awareness } from 'y-protocols/awareness'
import type * as Y from 'yjs'

import type { RoomName } from '../../types'

export interface Room {
doc: Y.Doc
awareness: Awareness
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export type AwarenessChanges = Record<'added' | 'updated' | 'removed', number[]>
import type { Room } from 'socket.io-adapter'
import type { Awareness } from 'y-protocols/awareness'

export type RoomName = Room
export type ClientId = Awareness['clientID']

0 comments on commit 282a1b4

Please sign in to comment.