Skip to content

Commit 990aa8c

Browse files
committed
live presence and ui tweaks
1 parent cff9289 commit 990aa8c

File tree

9 files changed

+178
-58
lines changed

9 files changed

+178
-58
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function createParameterStatusMessage(name: string, value: string): ArrayBuffer {
2+
const encoder = new TextEncoder()
3+
const nameBuffer = encoder.encode(name + '\0')
4+
const valueBuffer = encoder.encode(value + '\0')
5+
6+
const messageLength = 4 + nameBuffer.length + valueBuffer.length
7+
const message = new ArrayBuffer(1 + messageLength)
8+
const view = new DataView(message)
9+
const uint8Array = new Uint8Array(message)
10+
11+
let offset = 0
12+
view.setUint8(offset++, 'S'.charCodeAt(0)) // Message type
13+
view.setUint32(offset, messageLength, false) // Message length (big-endian)
14+
offset += 4
15+
16+
uint8Array.set(nameBuffer, offset)
17+
offset += nameBuffer.length
18+
19+
uint8Array.set(valueBuffer, offset)
20+
21+
return message
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { isIPv4 } from 'node:net'
2+
3+
export function extractIP(address: string): string {
4+
if (isIPv4(address)) {
5+
return address
6+
}
7+
8+
// Check if it's an IPv4-mapped IPv6 address
9+
const ipv4 = address.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/)
10+
if (ipv4) {
11+
return ipv4[1]
12+
}
13+
14+
// We assume it's an IPv6 address
15+
return address
16+
}

apps/browser-proxy/src/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import makeDebug from 'debug'
66
import * as tls from 'node:tls'
77
import { extractDatabaseId, isValidServername } from './servername.ts'
88
import { getTls } from './tls.ts'
9+
import { createParameterStatusMessage } from './create-message.ts'
10+
import { extractIP } from './extract-ip.ts'
911

1012
const debug = makeDebug('browser-proxy')
1113

@@ -127,6 +129,25 @@ tcpServer.on('connection', (socket) => {
127129
serverVersion() {
128130
return '16.3'
129131
},
132+
onAuthenticated() {
133+
const websocket = websocketConnections.get(databaseId!)
134+
135+
if (!websocket) {
136+
connection.sendError({
137+
code: 'XX000',
138+
message: 'the browser is not sharing the database',
139+
severity: 'FATAL',
140+
})
141+
connection.end()
142+
return
143+
}
144+
145+
const clientIpMessage = createParameterStatusMessage(
146+
'client_ip',
147+
extractIP(connection.socket.remoteAddress!)
148+
)
149+
websocket.send(clientIpMessage)
150+
},
130151
onMessage(message, state) {
131152
if (!state.isAuthenticated) {
132153
return
@@ -155,6 +176,8 @@ tcpServer.on('connection', (socket) => {
155176
socket.on('close', () => {
156177
if (databaseId) {
157178
tcpConnections.delete(databaseId)
179+
const websocket = websocketConnections.get(databaseId)
180+
websocket?.send(createParameterStatusMessage('client_ip', ''))
158181
}
159182
})
160183
})

apps/postgres-new/app/db/[id]/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Workspace from '~/components/workspace'
88
export default function Page({ params }: { params: { id: string } }) {
99
const databaseId = params.id
1010
const router = useRouter()
11-
const { dbManager, connectedDatabase } = useApp()
11+
const { dbManager, liveShare } = useApp()
1212

1313
useEffect(() => {
1414
async function run() {
@@ -25,12 +25,12 @@ export default function Page({ params }: { params: { id: string } }) {
2525
run()
2626
}, [dbManager, databaseId, router])
2727

28-
// Cleanup connected database when switching databases
28+
// Cleanup live shared database when switching databases
2929
useEffect(() => {
30-
if (connectedDatabase.isConnected && connectedDatabase.databaseId !== databaseId) {
31-
connectedDatabase.disconnect()
30+
if (liveShare.isLiveSharing && liveShare.databaseId !== databaseId) {
31+
liveShare.stop()
3232
}
33-
}, [connectedDatabase, databaseId])
33+
}, [liveShare, databaseId])
3434

3535
return <Workspace databaseId={databaseId} visibility="local" />
3636
}

apps/postgres-new/components/app-provider.tsx

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from 'react'
1818
import { DbManager } from '~/lib/db'
1919
import { useAsyncMemo } from '~/lib/hooks'
20+
import { parseParameterStatus } from '~/lib/pg-wire-util'
2021
import { createClient } from '~/utils/supabase/client'
2122

2223
export type AppProps = PropsWithChildren
@@ -105,9 +106,15 @@ export default function AppProvider({ children }: AppProps) {
105106
return await dbManager.getRuntimePgVersion()
106107
}, [dbManager])
107108

108-
const [connectedDatabaseId, setConnectedDatabaseId] = useState<string | null>(null)
109-
const [ws, setWs] = useState<WebSocket | null>(null)
110-
const connectDatabase = useCallback(
109+
const [liveSharedDatabaseId, setLiveSharedDatabaseId] = useState<string | null>(null)
110+
const [connectedClientIp, setConnectedClientIp] = useState<string | null>(null)
111+
const [liveShareWebsocket, setLiveShareWebsocket] = useState<WebSocket | null>(null)
112+
const cleanUp = useCallback(() => {
113+
setLiveShareWebsocket(null)
114+
setLiveSharedDatabaseId(null)
115+
setConnectedClientIp(null)
116+
}, [setLiveShareWebsocket, setLiveSharedDatabaseId, setConnectedClientIp])
117+
const startLiveShare = useCallback(
111118
async (databaseId: string) => {
112119
if (!dbManager) {
113120
throw new Error('dbManager is not available')
@@ -122,43 +129,53 @@ export default function AppProvider({ children }: AppProps) {
122129
ws.binaryType = 'arraybuffer'
123130

124131
ws.onopen = () => {
125-
setConnectedDatabaseId(databaseId)
132+
setLiveSharedDatabaseId(databaseId)
126133
}
127134
ws.onmessage = async (event) => {
128135
const message = new Uint8Array(await event.data)
136+
137+
const messageType = String.fromCharCode(message[0])
138+
if (messageType === 'S') {
139+
const { name, value } = parseParameterStatus(message)
140+
if (name === 'client_ip') {
141+
setConnectedClientIp(value === '' ? null : value)
142+
return
143+
}
144+
}
145+
129146
const response = await db.execProtocolRaw(message)
130147
ws.send(response)
131148
}
132149
ws.onclose = (event) => {
133-
setConnectedDatabaseId(null)
150+
cleanUp()
134151
}
135152
ws.onerror = (error) => {
136153
console.error('webSocket error:', error)
137-
setConnectedDatabaseId(null)
154+
cleanUp()
138155
}
139156

140-
setWs(ws)
157+
setLiveShareWebsocket(ws)
141158
},
142-
[dbManager]
159+
[dbManager, cleanUp]
143160
)
144-
const disconnectDatabase = useCallback(() => {
145-
ws?.close()
146-
setWs(null)
147-
setConnectedDatabaseId(null)
148-
}, [ws])
149-
const connectedDatabase = {
150-
connect: connectDatabase,
151-
disconnect: disconnectDatabase,
152-
databaseId: connectedDatabaseId,
153-
isConnected: Boolean(connectedDatabaseId),
161+
const stopLiveShare = useCallback(() => {
162+
liveShareWebsocket?.close()
163+
cleanUp()
164+
}, [cleanUp, liveShareWebsocket])
165+
const liveShare = {
166+
start: startLiveShare,
167+
stop: stopLiveShare,
168+
databaseId: liveSharedDatabaseId,
169+
clientIp: connectedClientIp,
170+
isLiveSharing: Boolean(liveSharedDatabaseId),
154171
}
155172

156173
return (
157174
<AppContext.Provider
158175
value={{
159176
user,
160177
isLoadingUser,
161-
connectedDatabase,
178+
liveShare,
162179
signIn,
163180
signOut,
164181
isSignInDialogOpen,
@@ -195,11 +212,12 @@ export type AppContextValues = {
195212
dbManager?: DbManager
196213
pgliteVersion?: string
197214
pgVersion?: string
198-
connectedDatabase: {
199-
connect: (databaseId: string) => Promise<void>
200-
disconnect: () => void
215+
liveShare: {
216+
start: (databaseId: string) => Promise<void>
217+
stop: () => void
201218
databaseId: string | null
202-
isConnected: boolean
219+
clientIp: string | null
220+
isLiveSharing: boolean
203221
}
204222
}
205223

apps/postgres-new/components/chat.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Message, generateId } from 'ai'
44
import { useChat } from 'ai/react'
55
import { AnimatePresence, m } from 'framer-motion'
6-
import { ArrowDown, ArrowUp, Flame, Paperclip, Square, WifiOff } from 'lucide-react'
6+
import { ArrowDown, ArrowUp, Flame, Paperclip, PlugIcon, Square } from 'lucide-react'
77
import {
88
ChangeEvent,
99
FormEventHandler,
@@ -49,7 +49,7 @@ export function getInitialMessages(tables: TablesData): Message[] {
4949
}
5050

5151
export default function Chat() {
52-
const { user, isLoadingUser, focusRef, setIsSignInDialogOpen, isRateLimited, connectedDatabase } =
52+
const { user, isLoadingUser, focusRef, setIsSignInDialogOpen, isRateLimited, liveShare } =
5353
useApp()
5454
const [inputFocusState, setInputFocusState] = useState(false)
5555

@@ -198,7 +198,7 @@ export default function Chat() {
198198
const [isMessageAnimationComplete, setIsMessageAnimationComplete] = useState(false)
199199

200200
const isChatEnabled =
201-
!isLoadingMessages && !isLoadingSchema && user !== undefined && !connectedDatabase.isConnected
201+
!isLoadingMessages && !isLoadingSchema && user !== undefined && !liveShare.isLiveSharing
202202

203203
const isSubmitEnabled = isChatEnabled && Boolean(input.trim())
204204

@@ -240,31 +240,34 @@ export default function Chat() {
240240
className={cn(
241241
'h-full flex flex-col items-center overflow-y-auto',
242242
!isMessageAnimationComplete ? 'overflow-x-hidden' : undefined,
243-
connectedDatabase.isConnected ? 'overflow-y-hidden' : undefined
243+
liveShare.isLiveSharing ? 'overflow-y-hidden' : undefined
244244
)}
245245
ref={scrollRef}
246246
>
247-
{connectedDatabase.isConnected && (
247+
{liveShare.isLiveSharing && (
248248
<div className="h-full w-full max-w-4xl flex flex-col gap-10 p-10 absolute backdrop-blur-sm bg-card/90">
249249
<div className="flex items-center justify-center h-full flex-col gap-y-5">
250250
<div className="w-full text-left">
251251
<p className="text-lg">Access your in-browser database</p>
252252
<p className="text-xs text-muted-foreground">
253-
Closing the window will disconnect your database
253+
Closing the window will stop the Live Share session
254254
</p>
255255
</div>
256256
<CopyableField
257-
value={`postgres://postgres@${connectedDatabase.databaseId}.${process.env.NEXT_PUBLIC_BROWSER_PROXY_DOMAIN}/postgres?sslmode=require`}
257+
value={`postgres://postgres@${liveShare.databaseId}.${process.env.NEXT_PUBLIC_BROWSER_PROXY_DOMAIN}/postgres?sslmode=require`}
258258
/>
259+
<p className="text-sm text-muted-foreground">
260+
{liveShare.clientIp ? `Connected from ${liveShare.clientIp}` : 'Not connected'}
261+
</p>
259262
<Button
260263
className="w-full gap-2"
261264
variant="outline"
262265
onClick={() => {
263-
connectedDatabase.disconnect()
266+
liveShare.stop()
264267
}}
265268
>
266-
<WifiOff className="h-[1.2rem] w-[1.2rem]" />
267-
<span>Disconnect database</span>
269+
<PlugIcon size={16} />
270+
<span>Stop sharing database</span>
268271
</Button>
269272
</div>
270273
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cx } from 'class-variance-authority'
2+
3+
export const LiveShareIcon = (props: { size?: number; className?: string }) => (
4+
<svg
5+
width={props.size}
6+
height={props.size}
7+
strokeWidth="1"
8+
viewBox="0 0 24 24"
9+
xmlns="http://www.w3.org/2000/svg"
10+
fill="currentColor"
11+
className={cx('lucide lucide-live-share', props.className)}
12+
>
13+
<path
14+
fill-rule="evenodd"
15+
clip-rule="evenodd"
16+
d="M13.735 1.694L15.178 1l8.029 6.328v1.388l-8.029 6.072-1.443-.694v-2.776h-.59c-4.06-.02-6.71.104-10.61 5.163l-1.534-.493a8.23 8.23 0 0 1 .271-2.255 11.026 11.026 0 0 1 3.92-6.793 11.339 11.339 0 0 1 7.502-2.547h1.04v-2.7zm1.804 7.917v2.776l5.676-4.281-5.648-4.545v2.664h-2.86A9.299 9.299 0 0 0 5.77 8.848a10.444 10.444 0 0 0-2.401 4.122c3.351-3.213 6.19-3.359 9.798-3.359h2.373zm-7.647 5.896a4.31 4.31 0 1 1 4.788 7.166 4.31 4.31 0 0 1-4.788-7.166zm.955 5.728a2.588 2.588 0 1 0 2.878-4.302 2.588 2.588 0 0 0-2.878 4.302z"
17+
/>
18+
</svg>
19+
)

0 commit comments

Comments
 (0)