Skip to content

Commit

Permalink
feat: basic permission control (TexteaInc#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
exuanbo committed Sep 12, 2022
1 parent 99fe576 commit 7a891d5
Show file tree
Hide file tree
Showing 18 changed files with 332 additions and 148 deletions.
3 changes: 0 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,5 @@ module.exports = {
next: {
rootDir: 'app'
}
},
rules: {
'no-void': ['error', { allowAsStatement: true }]
}
}
4 changes: 2 additions & 2 deletions bin/run.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env ts-node
import { createYDocServer } from '../src/server'
import { createSimpleServer } from '../src/server'

const host = process.env.HOST || 'localhost'
const port = +(process.env.PORT || 1234)

const server = createYDocServer()
const server = createSimpleServer()
server.listen(port, host, () => {
console.log(`running at '${host}' on port ${port}`)
})
10 changes: 9 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"server": "./scripts/server.ts",
"server:dev": "cross-env NODE_ENV=development DEBUG='socket.io:*' nodemon --watch scripts/server.ts --watch ../src/server scripts/server.ts"
},
"dependencies": {
"@textea/y-socket.io": "workspace:^",
Expand All @@ -15,10 +17,16 @@
"yjs": "^13.5.41"
},
"devDependencies": {
"@swc/core": "^1.2.242",
"@types/node": "^18.7.13",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/web": "^0.0.72",
"@vitejs/plugin-react": "^2.0.1",
"cross-env": "^7.0.3",
"nodemon": "^2.0.19",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
Expand Down
29 changes: 29 additions & 0 deletions example/scripts/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ts-node
import { createSocketIOServer } from '@textea/y-socket.io/server'
import { createServer } from 'http'

import { ClientData } from '../src/types'

const host = process.env.HOST || 'localhost'
const port = +(process.env.PORT || 1234)

const httpServer = createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'application/json' })
response.end('"okay"')
})

const io = createSocketIOServer<ClientData>(httpServer, {
autoDeleteRoom: process.env.NODE_ENV === 'development'
})

io.on('connection', (socket) => {
const { roomMap, roomName } = socket.yjs
const room = roomMap.get(roomName)!
socket.emit('data:update', {
isOwner: socket.userId === room.owner
})
})

httpServer.listen(port, host, () => {
console.log(`running at '${host}' on port ${port}`)
})
51 changes: 37 additions & 14 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import React, { useDeferredValue, useEffect, useState } from 'react'
import { Awareness } from 'y-protocols/awareness'
import * as Y from 'yjs'

import { ClientData } from './types'

const yDoc = new Y.Doc()
const yText = yDoc.getText()
const roomId = 'test-id'
Expand All @@ -23,13 +25,15 @@ awareness.setLocalState(DEFAULT_USER)

export const App: React.FC = () => {
const [text, setText] = useState('')
const [name, setName] = useState(DEFAULT_USER.name)
const [others, setOthers] = useState<User[]>([])
const [userName, setUserName] = useState(DEFAULT_USER.name)
const [otherUsers, setOtherUsers] = useState<User[]>([])

const [provider, setProvider] = useState<SocketIOProvider<ClientData>>()

const [provider, setProvider] = useState<SocketIOProvider>()
const isConnecting = useSocketIOProviderState(provider, (state) => state.connecting)
const isConnected = useSocketIOProviderState(provider, (state) => state.connected)
const isSynced = useSocketIOProviderState(provider, (state) => state.synced)

const deferredIsSynced = useDeferredValue(isSynced)

const status = isConnecting
Expand All @@ -40,24 +44,34 @@ export const App: React.FC = () => {
: 'Syncing'
: 'Disconnected'

const clientData = useSocketIOProviderState(provider, (state) => state.data)

const role = isConnected
? clientData
? clientData.isOwner
? 'Admin'
: 'User'
: 'Loading...'
: 'Not Available'

useEffect(() => {
const yTextObserver = () => {
setText(yText.toJSON())
}
yText.observe(yTextObserver)
const handleAwarenessUpdate = () => {
const self = awareness.getLocalState() as User | null
if (self) {
setName(self.name)
const localUser = awareness.getLocalState() as User | null
if (localUser) {
setUserName(localUser.name)
}
setOthers(
setOtherUsers(
[...awareness.getStates().entries()]
.filter(([clientId]) => clientId !== yDoc.clientID)
.map(([, state]) => state as User)
)
}
awareness.on('update', handleAwarenessUpdate)
const provider = createSocketIOProvider('ws:https://localhost:1234', roomId, yDoc, {
const provider = createSocketIOProvider<ClientData>('ws:https://localhost:1234', roomId, yDoc, {
awareness,
autoConnect: false
})
Expand All @@ -70,7 +84,7 @@ export const App: React.FC = () => {
}, [])

if (!provider) {
return <p>loading...</p>
return <p>Loading...</p>
}

return (
Expand All @@ -79,16 +93,25 @@ export const App: React.FC = () => {
<button
onClick={() => {
if (isConnected) {
provider?.disconnect()
provider.disconnect()
} else {
provider?.connect()
provider.connect()
}
}}
>
{isConnected ? 'Disconnect' : 'Connect'}
</button>
{role === 'Admin' && (
<>
<span>{' '}</span>
<button onClick={provider.closeRoom}>
Close Room
</button>
</>
)}
</p>
<p>Status: {status}</p>
<p>Role: {role}</p>
<p>
<label htmlFor='text'>Text: </label>
<input
Expand All @@ -106,17 +129,17 @@ export const App: React.FC = () => {
<label htmlFor='name'>Name: </label>
<input
id='name'
value={name}
value={userName}
onChange={(event) => {
awareness.setLocalStateField('name', event.target.value)
}}
/>
</p>
{others.length > 0 && (
{otherUsers.length > 0 && (
<div>
<div>Users: </div>
<ul>
{others.map((user) => (
{otherUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
Expand Down
3 changes: 3 additions & 0 deletions example/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ClientData = {
isOwner: boolean
}
12 changes: 10 additions & 2 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
// Completeness
"skipLibCheck": true
},
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src/**/*", "scripts/**/*"],
"references": [{ "path": "./tsconfig.node.json" }],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
},
"require": ["tsconfig-paths/register"],
"swc": true,
"transpileOnly": true
}
}
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"build": "tsc",
"prepublishOnly": "npm run build",
"start": "./bin/run.ts",
"start:dev": "cross-env NODE_ENV=development DEBUG='socket.io:*' nodemon --watch src/server ./bin/run.ts",
"example": "yarn workspace y-socket.io-example dev",
"example:server": "yarn workspace y-socket.io-example server:dev",
"test": "run -T vitest",
"test:ui": "run -T vitest --ui",
"test:run": "run -T vitest run"
Expand Down Expand Up @@ -57,7 +57,6 @@
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"@vitest/ui": "^0.22.1",
"cross-env": "^7.0.3",
"eslint": "^8.22.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-cypress": "^2.12.1",
Expand All @@ -70,7 +69,6 @@
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"nodemon": "^2.0.19",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4",
Expand Down
18 changes: 14 additions & 4 deletions src/events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientId } from './types'
import type { ClientId, DefaultClientData } from './types'

type EventHandler = (...args: any[]) => void
export type DefaultEvents = {
Expand All @@ -7,12 +7,15 @@ export type DefaultEvents = {

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

type DataScope = 'data'
type RoomScope = 'room'

type YDocScope = 'doc'
type AwarenessScope = 'awareness'
type ObservableScope = YDocScope | AwarenessScope
type ObservableEventName = EventNameWithScope<ObservableScope>

type ValidEventScope = ObservableScope
type ValidEventScope = DataScope | RoomScope | ObservableScope

type ValidateEvents<
Events extends DefaultEvents & {
Expand All @@ -24,18 +27,25 @@ type ValidateEvents<
}
> = Events

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

export type ClientToServerEvents = ValidateEvents<{
export interface ServerToClientEvents<ClientData extends DefaultClientData = DefaultClientData>
extends DefaultServerToClientEvents<ClientData> {}

export type DefaultClientToServerEvents = ValidateEvents<{
['room:close']: () => void
['doc:diff']: (diff: Uint8Array) => void
['doc:update']: (updateV2: Uint8Array, callback?: () => void) => void
['awareness:update']: (update: Uint8Array) => void
}>

export interface ClientToServerEvents extends DefaultClientToServerEvents {}

type ClientToServerEventNames = keyof ClientToServerEvents

export type BroadcastChannelMessageData<EventName extends ClientToServerEventNames = ClientToServerEventNames> =
Expand Down
22 changes: 14 additions & 8 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@ import { useDebugValue } from 'react'
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'

import { INITIAL_STATE, SocketIOProvider, SocketIOProviderState } from './provider'
import type { DefaultClientData } from './types'

const getInitialState = () => INITIAL_STATE

const noop = () => noop
const identity = <T>(value: T) => value

export function useSocketIOProviderState (provider: SocketIOProvider | null | undefined): SocketIOProviderState
export function useSocketIOProviderState <ClientData extends DefaultClientData>(
provider: SocketIOProvider<ClientData> | null | undefined
): SocketIOProviderState<ClientData>

export function useSocketIOProviderState<StateSlice> (
provider: SocketIOProvider | null | undefined,
selector: (state: SocketIOProviderState) => StateSlice,
export function useSocketIOProviderState<ClientData extends DefaultClientData, StateSlice> (
provider: SocketIOProvider<ClientData> | null | undefined,
selector: (state: SocketIOProviderState<ClientData>) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
): StateSlice

export function useSocketIOProviderState<StateSlice> (
provider: SocketIOProvider | null | undefined,
selector: (state: SocketIOProviderState) => StateSlice | SocketIOProviderState = identity,
equalityFn?: (a: StateSlice | SocketIOProviderState, b: StateSlice | SocketIOProviderState) => boolean
export function useSocketIOProviderState<ClientData extends DefaultClientData, StateSlice> (
provider: SocketIOProvider<ClientData> | null | undefined,
selector: (state: SocketIOProviderState<ClientData>) => StateSlice | SocketIOProviderState<ClientData> = identity,
equalityFn?: (
a: StateSlice | SocketIOProviderState<ClientData>,
b: StateSlice | SocketIOProviderState<ClientData>
) => boolean
) {
const { getState = getInitialState, subscribe = noop } = provider ?? {}
const selectedState = useSyncExternalStoreWithSelector(
Expand Down
Loading

0 comments on commit 7a891d5

Please sign in to comment.