Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic permission control #13

Merged
merged 17 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
himself65 marked this conversation as resolved.
Show resolved Hide resolved
['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