@owlmeans/client-socket 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/build/.gitkeep +0 -0
- package/build/helper.d.ts +8 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +41 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +7 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/connection.d.ts +4 -0
- package/build/utils/connection.d.ts.map +1 -0
- package/build/utils/connection.js +44 -0
- package/build/utils/connection.js.map +1 -0
- package/build/utils/index.d.ts +2 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +2 -0
- package/build/utils/index.js.map +1 -0
- package/package.json +44 -0
- package/src/helper.ts +53 -0
- package/src/index.ts +3 -0
- package/src/types.ts +7 -0
- package/src/utils/connection.ts +54 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# @owlmeans/client-socket
|
|
2
|
+
|
|
3
|
+
The **@owlmeans/client-socket** package provides client-side WebSocket integration for OwlMeans Common Libraries, enabling real-time communication between React frontend applications and backend services through WebSocket connections.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package serves as the client-side WebSocket implementation, specifically designed for:
|
|
8
|
+
|
|
9
|
+
- **React WebSocket Integration**: Seamless WebSocket connections in React applications
|
|
10
|
+
- **Module-based Socket Management**: Integration with the OwlMeans module system for WebSocket endpoints
|
|
11
|
+
- **Connection Lifecycle Management**: Automatic connection establishment, maintenance, and cleanup
|
|
12
|
+
- **Authentication Integration**: Built-in authentication support for secure WebSocket connections
|
|
13
|
+
- **Real-time Communication**: Enable real-time features like chat, notifications, and live updates
|
|
14
|
+
- **TypeScript Support**: Full TypeScript support with proper type safety
|
|
15
|
+
|
|
16
|
+
## Key Concepts
|
|
17
|
+
|
|
18
|
+
### Client-side WebSocket Management
|
|
19
|
+
This package provides client-side WebSocket functionality that integrates with the OwlMeans module system, allowing WebSocket connections to be defined as modules.
|
|
20
|
+
|
|
21
|
+
### Connection Abstraction
|
|
22
|
+
Wraps native WebSocket connections in a unified `Connection` interface that provides consistent API for message handling, authentication, and lifecycle management.
|
|
23
|
+
|
|
24
|
+
### React Integration
|
|
25
|
+
Provides React hooks for managing WebSocket connections with automatic cleanup and re-connection logic.
|
|
26
|
+
|
|
27
|
+
### Module-driven Connections
|
|
28
|
+
WebSocket endpoints are defined as modules, ensuring consistent URL generation and authentication handling across the application.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @owlmeans/client-socket
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Reference
|
|
37
|
+
|
|
38
|
+
### Types
|
|
39
|
+
|
|
40
|
+
#### `Config`
|
|
41
|
+
Configuration interface extending ClientConfig.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface Config extends ClientConfig { }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### `Context<C extends Config = Config>`
|
|
48
|
+
Context interface for client applications with socket support.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Core Functions
|
|
55
|
+
|
|
56
|
+
#### `ws(module: ClientModule<string>, request?: AbstractRequest<{ token?: string }>): Promise<Connection>`
|
|
57
|
+
|
|
58
|
+
Creates a WebSocket connection using a client module.
|
|
59
|
+
|
|
60
|
+
**Parameters:**
|
|
61
|
+
- `module`: Client module that defines the WebSocket endpoint
|
|
62
|
+
- `request`: Optional request object with authentication token
|
|
63
|
+
|
|
64
|
+
**Returns:** Promise that resolves to a Connection instance
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { ws } from '@owlmeans/client-socket'
|
|
68
|
+
|
|
69
|
+
const socketModule = context.module<ClientModule>('chat-socket')
|
|
70
|
+
const connection = await ws(socketModule, {
|
|
71
|
+
query: { token: authToken }
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### React Hooks
|
|
76
|
+
|
|
77
|
+
#### `useWs(module: string | ClientModule<any>, request?: Partial<AbstractRequest<any>>): Connection | null`
|
|
78
|
+
|
|
79
|
+
React hook for managing WebSocket connections with automatic lifecycle management.
|
|
80
|
+
|
|
81
|
+
**Parameters:**
|
|
82
|
+
- `module`: Module alias or ClientModule instance
|
|
83
|
+
- `request`: Optional request parameters
|
|
84
|
+
|
|
85
|
+
**Returns:** Connection instance or null if not connected
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
89
|
+
|
|
90
|
+
function ChatComponent() {
|
|
91
|
+
const connection = useWs('chat-socket', {
|
|
92
|
+
query: { room: 'general' }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (connection) {
|
|
97
|
+
connection.on('message', handleMessage)
|
|
98
|
+
}
|
|
99
|
+
}, [connection])
|
|
100
|
+
|
|
101
|
+
return <div>Chat interface</div>
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Utility Functions
|
|
106
|
+
|
|
107
|
+
#### `makeConnection<C extends Config = Config, T extends Context<C> = Context<C>>(conn: WebSocket, context: T): Connection`
|
|
108
|
+
|
|
109
|
+
Creates a Connection wrapper around a native WebSocket instance.
|
|
110
|
+
|
|
111
|
+
**Parameters:**
|
|
112
|
+
- `conn`: Native WebSocket instance
|
|
113
|
+
- `context`: Application context
|
|
114
|
+
|
|
115
|
+
**Returns:** Connection wrapper with enhanced functionality
|
|
116
|
+
|
|
117
|
+
## Usage Examples
|
|
118
|
+
|
|
119
|
+
### Basic WebSocket Connection
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
123
|
+
import { useContext } from '@owlmeans/client'
|
|
124
|
+
import { useEffect, useState } from 'react'
|
|
125
|
+
|
|
126
|
+
function WebSocketExample() {
|
|
127
|
+
const [messages, setMessages] = useState<string[]>([])
|
|
128
|
+
const connection = useWs('websocket-endpoint')
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (connection) {
|
|
132
|
+
connection.on('message', (message) => {
|
|
133
|
+
setMessages(prev => [...prev, message.payload])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Send a message
|
|
137
|
+
connection.send({
|
|
138
|
+
type: 'text',
|
|
139
|
+
payload: 'Hello WebSocket!'
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}, [connection])
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div>
|
|
146
|
+
<h3>Messages:</h3>
|
|
147
|
+
{messages.map((msg, idx) => (
|
|
148
|
+
<div key={idx}>{msg}</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Authenticated WebSocket Connection
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
159
|
+
import { useAuth } from '@owlmeans/client-auth'
|
|
160
|
+
import { useEffect } from 'react'
|
|
161
|
+
|
|
162
|
+
function AuthenticatedSocket() {
|
|
163
|
+
const auth = useAuth()
|
|
164
|
+
const connection = useWs('secure-socket', {
|
|
165
|
+
query: {
|
|
166
|
+
token: auth.getToken()
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (connection) {
|
|
172
|
+
connection.on('authenticated', () => {
|
|
173
|
+
console.log('Socket authenticated successfully')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
connection.on('auth-error', (error) => {
|
|
177
|
+
console.error('Socket authentication failed:', error)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}, [connection])
|
|
181
|
+
|
|
182
|
+
return <div>Authenticated WebSocket connection</div>
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Chat Application
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
190
|
+
import { useState, useEffect } from 'react'
|
|
191
|
+
|
|
192
|
+
interface ChatMessage {
|
|
193
|
+
id: string
|
|
194
|
+
user: string
|
|
195
|
+
message: string
|
|
196
|
+
timestamp: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
200
|
+
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
201
|
+
const [inputMessage, setInputMessage] = useState('')
|
|
202
|
+
|
|
203
|
+
const connection = useWs('chat-socket', {
|
|
204
|
+
query: { room: roomId }
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (connection) {
|
|
209
|
+
connection.on('chat-message', (event) => {
|
|
210
|
+
const message: ChatMessage = event.payload
|
|
211
|
+
setMessages(prev => [...prev, message])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
connection.on('user-joined', (event) => {
|
|
215
|
+
console.log(`${event.payload.user} joined the room`)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
connection.on('user-left', (event) => {
|
|
219
|
+
console.log(`${event.payload.user} left the room`)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}, [connection, roomId])
|
|
223
|
+
|
|
224
|
+
const sendMessage = () => {
|
|
225
|
+
if (connection && inputMessage.trim()) {
|
|
226
|
+
connection.send({
|
|
227
|
+
type: 'chat-message',
|
|
228
|
+
payload: {
|
|
229
|
+
message: inputMessage,
|
|
230
|
+
room: roomId
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
setInputMessage('')
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<div className="messages">
|
|
240
|
+
{messages.map(msg => (
|
|
241
|
+
<div key={msg.id}>
|
|
242
|
+
<strong>{msg.user}:</strong> {msg.message}
|
|
243
|
+
</div>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
<div className="input">
|
|
247
|
+
<input
|
|
248
|
+
value={inputMessage}
|
|
249
|
+
onChange={(e) => setInputMessage(e.target.value)}
|
|
250
|
+
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
|
251
|
+
/>
|
|
252
|
+
<button onClick={sendMessage}>Send</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Real-time Notifications
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
263
|
+
import { useEffect, useState } from 'react'
|
|
264
|
+
|
|
265
|
+
interface Notification {
|
|
266
|
+
id: string
|
|
267
|
+
type: 'info' | 'warning' | 'error' | 'success'
|
|
268
|
+
title: string
|
|
269
|
+
message: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function NotificationSystem() {
|
|
273
|
+
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
274
|
+
const connection = useWs('notification-socket')
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (connection) {
|
|
278
|
+
connection.on('notification', (event) => {
|
|
279
|
+
const notification: Notification = event.payload
|
|
280
|
+
setNotifications(prev => [notification, ...prev])
|
|
281
|
+
|
|
282
|
+
// Auto-remove after 5 seconds
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
setNotifications(prev =>
|
|
285
|
+
prev.filter(n => n.id !== notification.id)
|
|
286
|
+
)
|
|
287
|
+
}, 5000)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
connection.on('notification-clear', (event) => {
|
|
291
|
+
const { notificationId } = event.payload
|
|
292
|
+
setNotifications(prev =>
|
|
293
|
+
prev.filter(n => n.id !== notificationId)
|
|
294
|
+
)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}, [connection])
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="notification-container">
|
|
301
|
+
{notifications.map(notification => (
|
|
302
|
+
<div key={notification.id} className={`notification ${notification.type}`}>
|
|
303
|
+
<h4>{notification.title}</h4>
|
|
304
|
+
<p>{notification.message}</p>
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Live Data Updates
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
316
|
+
import { useState, useEffect } from 'react'
|
|
317
|
+
|
|
318
|
+
interface LiveData {
|
|
319
|
+
timestamp: number
|
|
320
|
+
value: number
|
|
321
|
+
status: string
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function LiveDataDashboard() {
|
|
325
|
+
const [data, setData] = useState<LiveData[]>([])
|
|
326
|
+
const connection = useWs('live-data-socket')
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (connection) {
|
|
330
|
+
connection.on('data-update', (event) => {
|
|
331
|
+
const newData: LiveData = event.payload
|
|
332
|
+
setData(prev => {
|
|
333
|
+
const updated = [newData, ...prev.slice(0, 99)] // Keep last 100 entries
|
|
334
|
+
return updated
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Request initial data
|
|
339
|
+
connection.send({
|
|
340
|
+
type: 'request-initial-data',
|
|
341
|
+
payload: {}
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}, [connection])
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div>
|
|
348
|
+
<h2>Live Data Stream</h2>
|
|
349
|
+
<div className="data-grid">
|
|
350
|
+
{data.map((item, idx) => (
|
|
351
|
+
<div key={idx} className="data-item">
|
|
352
|
+
<span>Value: {item.value}</span>
|
|
353
|
+
<span>Status: {item.status}</span>
|
|
354
|
+
<span>Time: {new Date(item.timestamp).toLocaleTimeString()}</span>
|
|
355
|
+
</div>
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Connection Status Management
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
367
|
+
import { useState, useEffect } from 'react'
|
|
368
|
+
|
|
369
|
+
function ConnectionStatusProvider({ children }: { children: React.ReactNode }) {
|
|
370
|
+
const [isConnected, setIsConnected] = useState(false)
|
|
371
|
+
const [connectionError, setConnectionError] = useState<string | null>(null)
|
|
372
|
+
const connection = useWs('main-socket')
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (connection) {
|
|
376
|
+
setIsConnected(true)
|
|
377
|
+
setConnectionError(null)
|
|
378
|
+
|
|
379
|
+
connection.on('close', () => {
|
|
380
|
+
setIsConnected(false)
|
|
381
|
+
setConnectionError('Connection lost')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
connection.on('error', (event) => {
|
|
385
|
+
setConnectionError(event.payload.message)
|
|
386
|
+
})
|
|
387
|
+
} else {
|
|
388
|
+
setIsConnected(false)
|
|
389
|
+
}
|
|
390
|
+
}, [connection])
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div>
|
|
394
|
+
<div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
|
|
395
|
+
{isConnected ? 'Connected' : 'Disconnected'}
|
|
396
|
+
{connectionError && <span>: {connectionError}</span>}
|
|
397
|
+
</div>
|
|
398
|
+
{children}
|
|
399
|
+
</div>
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Manual Connection Management
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { ws } from '@owlmeans/client-socket'
|
|
408
|
+
import { useContext } from '@owlmeans/client'
|
|
409
|
+
import { useEffect, useState } from 'react'
|
|
410
|
+
|
|
411
|
+
function ManualConnectionComponent() {
|
|
412
|
+
const context = useContext()
|
|
413
|
+
const [connection, setConnection] = useState<Connection | null>(null)
|
|
414
|
+
const [status, setStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected')
|
|
415
|
+
|
|
416
|
+
const connect = async () => {
|
|
417
|
+
try {
|
|
418
|
+
setStatus('connecting')
|
|
419
|
+
const socketModule = context.module<ClientModule>('manual-socket')
|
|
420
|
+
const conn = await ws(socketModule)
|
|
421
|
+
|
|
422
|
+
conn.on('close', () => {
|
|
423
|
+
setStatus('disconnected')
|
|
424
|
+
setConnection(null)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
setConnection(conn)
|
|
428
|
+
setStatus('connected')
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error('Connection failed:', error)
|
|
431
|
+
setStatus('disconnected')
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const disconnect = async () => {
|
|
436
|
+
if (connection) {
|
|
437
|
+
await connection.close()
|
|
438
|
+
setConnection(null)
|
|
439
|
+
setStatus('disconnected')
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
return () => {
|
|
445
|
+
if (connection) {
|
|
446
|
+
connection.close()
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}, [])
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
<div>
|
|
453
|
+
<div>Status: {status}</div>
|
|
454
|
+
<button onClick={connect} disabled={status !== 'disconnected'}>
|
|
455
|
+
Connect
|
|
456
|
+
</button>
|
|
457
|
+
<button onClick={disconnect} disabled={status === 'disconnected'}>
|
|
458
|
+
Disconnect
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Error Handling
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
469
|
+
import { useEffect, useState } from 'react'
|
|
470
|
+
|
|
471
|
+
function ErrorHandlingExample() {
|
|
472
|
+
const [error, setError] = useState<string | null>(null)
|
|
473
|
+
const connection = useWs('error-prone-socket')
|
|
474
|
+
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
if (connection) {
|
|
477
|
+
connection.on('error', (event) => {
|
|
478
|
+
setError(event.payload.message || 'Unknown error occurred')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
connection.on('close', (event) => {
|
|
482
|
+
if (event.payload.code !== 1000) { // Not a normal closure
|
|
483
|
+
setError(`Connection closed unexpectedly (Code: ${event.payload.code})`)
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Clear error when reconnected
|
|
488
|
+
connection.on('open', () => {
|
|
489
|
+
setError(null)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
}, [connection])
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<div>
|
|
496
|
+
{error && (
|
|
497
|
+
<div className="error-message">
|
|
498
|
+
Error: {error}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
<div>
|
|
502
|
+
Connection Status: {connection ? 'Connected' : 'Disconnected'}
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Integration Patterns
|
|
510
|
+
|
|
511
|
+
### Module Definition
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// Define WebSocket module in your module configuration
|
|
515
|
+
import { module } from '@owlmeans/client-module'
|
|
516
|
+
import { route } from '@owlmeans/route'
|
|
517
|
+
|
|
518
|
+
const chatSocketModule = module(
|
|
519
|
+
route('chat-socket', '/ws/chat'),
|
|
520
|
+
guard('authenticated')
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
// Register the module with your context
|
|
524
|
+
context.registerModule(chatSocketModule)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Context Provider Pattern
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import React, { createContext, useContext as useReactContext } from 'react'
|
|
531
|
+
import { Connection } from '@owlmeans/socket'
|
|
532
|
+
import { useWs } from '@owlmeans/client-socket'
|
|
533
|
+
|
|
534
|
+
const SocketContext = createContext<Connection | null>(null)
|
|
535
|
+
|
|
536
|
+
export function SocketProvider({ children }: { children: React.ReactNode }) {
|
|
537
|
+
const connection = useWs('global-socket')
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<SocketContext.Provider value={connection}>
|
|
541
|
+
{children}
|
|
542
|
+
</SocketContext.Provider>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function useGlobalSocket() {
|
|
547
|
+
const connection = useReactContext(SocketContext)
|
|
548
|
+
if (!connection) {
|
|
549
|
+
throw new Error('useGlobalSocket must be used within SocketProvider')
|
|
550
|
+
}
|
|
551
|
+
return connection
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Best Practices
|
|
556
|
+
|
|
557
|
+
1. **Connection Management**: Always handle connection lifecycle properly with cleanup
|
|
558
|
+
2. **Error Handling**: Implement comprehensive error handling for network issues
|
|
559
|
+
3. **Reconnection Logic**: Consider implementing automatic reconnection for critical connections
|
|
560
|
+
4. **Message Validation**: Validate incoming messages to ensure data integrity
|
|
561
|
+
5. **Performance**: Avoid creating too many simultaneous WebSocket connections
|
|
562
|
+
6. **Authentication**: Securely handle authentication tokens in WebSocket connections
|
|
563
|
+
7. **Memory Leaks**: Ensure proper cleanup of event listeners and connections
|
|
564
|
+
|
|
565
|
+
## Dependencies
|
|
566
|
+
|
|
567
|
+
This package depends on:
|
|
568
|
+
- `@owlmeans/auth` - Authentication framework
|
|
569
|
+
- `@owlmeans/client` - Client framework
|
|
570
|
+
- `@owlmeans/client-context` - Client context management
|
|
571
|
+
- `@owlmeans/client-module` - Client module system
|
|
572
|
+
- `@owlmeans/context` - Context management
|
|
573
|
+
- `@owlmeans/module` - Module system
|
|
574
|
+
- `@owlmeans/socket` - Core socket functionality
|
|
575
|
+
- `react` - React framework
|
|
576
|
+
|
|
577
|
+
## Related Packages
|
|
578
|
+
|
|
579
|
+
- [`@owlmeans/socket`](../socket) - Core socket functionality
|
|
580
|
+
- [`@owlmeans/server-socket`](../server-socket) - Server-side WebSocket implementation
|
|
581
|
+
- [`@owlmeans/client`](../client) - Client framework
|
|
582
|
+
- [`@owlmeans/client-module`](../client-module) - Client module system
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ClientModule } from '@owlmeans/client-module';
|
|
2
|
+
import type { AbstractRequest } from '@owlmeans/module';
|
|
3
|
+
import type { Connection } from '@owlmeans/socket';
|
|
4
|
+
export declare const ws: (module: ClientModule<string>, request?: AbstractRequest<{
|
|
5
|
+
token?: string;
|
|
6
|
+
}>) => Promise<Connection>;
|
|
7
|
+
export declare const useWs: (module: string | ClientModule<any>, request?: Partial<AbstractRequest<any>>) => Connection | null;
|
|
8
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAUlD,eAAO,MAAM,EAAE,WAAkB,YAAY,CAAC,MAAM,CAAC,YAAY,eAAe,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KAAG,OAAO,CAAC,UAAU,CAexH,CAAA;AAED,eAAO,MAAM,KAAK,WAAY,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,YAAY,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,KAAG,UAAU,GAAG,IAqBhH,CAAA"}
|
package/build/helper.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { provideRequest } from '@owlmeans/client-module';
|
|
2
|
+
import { ModuleOutcome } from '@owlmeans/module';
|
|
3
|
+
import { SocketInitializationError } from '@owlmeans/socket';
|
|
4
|
+
import { makeConnection } from './utils/connection.js';
|
|
5
|
+
import { assertContext } from '@owlmeans/context';
|
|
6
|
+
import { useContext, useValue } from '@owlmeans/client';
|
|
7
|
+
import { AUTH_QUERY } from '@owlmeans/auth';
|
|
8
|
+
import { urlCall } from '@owlmeans/client-module/utils';
|
|
9
|
+
import { useEffect, useMemo } from 'react';
|
|
10
|
+
export const ws = async (module, request) => {
|
|
11
|
+
const ctx = assertContext(module.ctx, 'client-ws');
|
|
12
|
+
request = request ?? provideRequest(module.getAlias(), module.getPath());
|
|
13
|
+
const [url, state] = await urlCall({ ref: module })(request);
|
|
14
|
+
if (state !== ModuleOutcome.Ok) {
|
|
15
|
+
throw new SocketInitializationError;
|
|
16
|
+
}
|
|
17
|
+
const socket = new WebSocket(url);
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
socket.onopen = () => {
|
|
20
|
+
resolve(makeConnection(socket, ctx));
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const useWs = (module, request) => {
|
|
25
|
+
const ctx = useContext();
|
|
26
|
+
const mod = useMemo(() => typeof module === 'string' ? ctx.module(module) : module, [module]);
|
|
27
|
+
const connection = useValue(async () => {
|
|
28
|
+
const _request = provideRequest(mod.getAlias(), mod.getPath());
|
|
29
|
+
Object.assign(_request, request);
|
|
30
|
+
return await ws(mod, _request);
|
|
31
|
+
}, [mod.getAlias(), request?.query?.[AUTH_QUERY]]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (connection != null) {
|
|
34
|
+
return () => {
|
|
35
|
+
void connection.close();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}, [connection]);
|
|
39
|
+
return connection;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE1C,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,EAAE,MAA4B,EAAE,OAA6C,EAAuB,EAAE;IAC3H,MAAM,GAAG,GAAG,aAAa,CAAkB,MAAM,CAAC,GAAc,EAAE,WAAW,CAAC,CAAA;IAC9E,OAAO,GAAG,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;IACxE,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IAE5D,IAAI,KAAK,KAAK,aAAa,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,yBAAyB,CAAA;IACrC,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,CAAA;IAEjC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE;YACnB,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;QACtC,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,MAAkC,EAAE,OAAuC,EAAqB,EAAE;IACtH,MAAM,GAAG,GAAG,UAAU,EAAE,CAAA;IACxB,MAAM,GAAG,GAAG,OAAO,CACjB,GAAG,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAe,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAC5E,CAAC,MAAM,CAAC,CACT,CAAA;IACD,MAAM,UAAU,GAAG,QAAQ,CAAa,KAAK,IAAI,EAAE;QACjD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChC,OAAO,MAAM,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAChC,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAElD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;YACvB,OAAO,GAAG,EAAE;gBACV,KAAK,UAAU,CAAC,KAAK,EAAE,CAAA;YACzB,CAAC,CAAA;QACH,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEhB,OAAO,UAAU,CAAA;AACnB,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientConfig } from '@owlmeans/client-context';
|
|
2
|
+
import type { ClientContext } from '@owlmeans/client';
|
|
3
|
+
export interface Config extends ClientConfig {
|
|
4
|
+
}
|
|
5
|
+
export interface Context<C extends Config = Config> extends ClientContext<C> {
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,WAAW,MAAO,SAAQ,YAAY;CAAI;AAEhD,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Config, Context } from '../types.js';
|
|
2
|
+
import type { Connection } from '@owlmeans/socket';
|
|
3
|
+
export declare const makeConnection: <C extends Config = Config, T extends Context<C> = Context<C>>(conn: WebSocket, _context: T) => Connection;
|
|
4
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/utils/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAA4B,MAAM,kBAAkB,CAAA;AAG5E,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,MAAM,WAAW,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,qBACtE,SAAS,YAAY,CAAC,KAC3B,UA+CF,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createBasicConnection, MessageType } from '@owlmeans/socket';
|
|
2
|
+
export const makeConnection = (conn, _context) => {
|
|
3
|
+
const model = createBasicConnection();
|
|
4
|
+
model.send = async (message) => {
|
|
5
|
+
if (typeof message !== 'string') {
|
|
6
|
+
model.prepare?.(message);
|
|
7
|
+
}
|
|
8
|
+
conn.send(typeof message === 'string' ? message : JSON.stringify(message));
|
|
9
|
+
};
|
|
10
|
+
model.close = async () => {
|
|
11
|
+
// await closeHandler(new CloseEvent('close', { code: 1000, wasClean: true }))
|
|
12
|
+
conn.close();
|
|
13
|
+
// @TODO Make sure it trigger close observers
|
|
14
|
+
};
|
|
15
|
+
model.authenticate = async (_stage, _payload) => {
|
|
16
|
+
// @TODO Provide authentication flow logic here
|
|
17
|
+
return [];
|
|
18
|
+
};
|
|
19
|
+
model.prepare = (message, _isRequest) => {
|
|
20
|
+
// @TODO add sender / recipient metadata
|
|
21
|
+
message.dt = message.dt ?? Date.now();
|
|
22
|
+
return message;
|
|
23
|
+
};
|
|
24
|
+
const messageHandler = async (message) => {
|
|
25
|
+
await model.receive(message.data);
|
|
26
|
+
};
|
|
27
|
+
const closeHandler = async (event) => {
|
|
28
|
+
const msg = {
|
|
29
|
+
type: MessageType.System,
|
|
30
|
+
event: 'close',
|
|
31
|
+
payload: { code: event.code }
|
|
32
|
+
};
|
|
33
|
+
if (model.prepare != null) {
|
|
34
|
+
model.prepare(msg);
|
|
35
|
+
}
|
|
36
|
+
await Promise.all(model.getListeners().map(async (listener) => listener(msg)));
|
|
37
|
+
conn.removeEventListener('message', messageHandler);
|
|
38
|
+
conn.removeEventListener('close', closeHandler);
|
|
39
|
+
};
|
|
40
|
+
conn.addEventListener('message', messageHandler);
|
|
41
|
+
conn.addEventListener('close', closeHandler);
|
|
42
|
+
return model;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/utils/connection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAErE,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,IAAe,EAAE,QAAW,EAChB,EAAE;IACd,MAAM,KAAK,GAAG,qBAAqB,EAAE,CAAA;IAErC,KAAK,CAAC,IAAI,GAAG,KAAK,EAAC,OAAO,EAAC,EAAE;QAC3B,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IAC5E,CAAC,CAAA;IAED,KAAK,CAAC,KAAK,GAAG,KAAK,IAAI,EAAE;QACvB,8EAA8E;QAC9E,IAAI,CAAC,KAAK,EAAE,CAAA;QACZ,6CAA6C;IAC/C,CAAC,CAAA;IAED,KAAK,CAAC,YAAY,GAAG,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;QAC9C,+CAA+C;QAC/C,OAAO,EAAS,CAAA;IAClB,CAAC,CAAA;IAED,KAAK,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE;QACtC,wCAAwC;QACxC,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAA;QACrC,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;IAED,MAAM,cAAc,GAAG,KAAK,EAAE,OAAqB,EAAE,EAAE;QACrD,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,CAAC,CAAA;IACD,MAAM,YAAY,GAAG,KAAK,EAAE,KAAiB,EAAE,EAAE;QAC/C,MAAM,GAAG,GAA+B;YACtC,IAAI,EAAE,WAAW,CAAC,MAAM;YACxB,KAAK,EAAE,OAAO;YACd,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE;SAC9B,CAAA;QACD,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;YAC1B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACpB,CAAC;QACD,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,KAAK,EAAC,QAAQ,EAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC5E,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QACnD,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IACjD,CAAC,CAAA;IACD,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;IAChD,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAE5C,OAAO,KAAK,CAAA;AACd,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/client-socket",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 132 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": "*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.3.11",
|
|
27
|
+
"nodemon": "^3.1.7",
|
|
28
|
+
"typescript": "^5.6.3"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@owlmeans/auth": "^0.1.0",
|
|
32
|
+
"@owlmeans/basic-envelope": "^0.1.0",
|
|
33
|
+
"@owlmeans/client": "^0.1.0",
|
|
34
|
+
"@owlmeans/client-context": "^0.1.0",
|
|
35
|
+
"@owlmeans/client-module": "^0.1.0",
|
|
36
|
+
"@owlmeans/context": "^0.1.0",
|
|
37
|
+
"@owlmeans/module": "^0.1.0",
|
|
38
|
+
"@owlmeans/socket": "^0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"private": false,
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ClientModule } from '@owlmeans/client-module'
|
|
2
|
+
import type { AbstractRequest } from '@owlmeans/module'
|
|
3
|
+
import { provideRequest } from '@owlmeans/client-module'
|
|
4
|
+
import { ModuleOutcome } from '@owlmeans/module'
|
|
5
|
+
import type { Connection } from '@owlmeans/socket'
|
|
6
|
+
import { SocketInitializationError } from '@owlmeans/socket'
|
|
7
|
+
import { makeConnection } from './utils/connection.js'
|
|
8
|
+
import { assertContext } from '@owlmeans/context'
|
|
9
|
+
import type { Config, Context } from './types.js'
|
|
10
|
+
import { useContext, useValue } from '@owlmeans/client'
|
|
11
|
+
import { AUTH_QUERY } from '@owlmeans/auth'
|
|
12
|
+
import { urlCall } from '@owlmeans/client-module/utils'
|
|
13
|
+
import { useEffect, useMemo } from 'react'
|
|
14
|
+
|
|
15
|
+
export const ws = async (module: ClientModule<string>, request?: AbstractRequest<{ token?: string }>): Promise<Connection> => {
|
|
16
|
+
const ctx = assertContext<Config, Context>(module.ctx as Context, 'client-ws')
|
|
17
|
+
request = request ?? provideRequest(module.getAlias(), module.getPath())
|
|
18
|
+
const [url, state] = await urlCall({ ref: module })(request)
|
|
19
|
+
|
|
20
|
+
if (state !== ModuleOutcome.Ok) {
|
|
21
|
+
throw new SocketInitializationError
|
|
22
|
+
}
|
|
23
|
+
const socket = new WebSocket(url)
|
|
24
|
+
|
|
25
|
+
return new Promise(resolve => {
|
|
26
|
+
socket.onopen = () => {
|
|
27
|
+
resolve(makeConnection(socket, ctx))
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useWs = (module: string | ClientModule<any>, request?: Partial<AbstractRequest<any>>): Connection | null => {
|
|
33
|
+
const ctx = useContext()
|
|
34
|
+
const mod = useMemo(
|
|
35
|
+
() => typeof module === 'string' ? ctx.module<ClientModule>(module) : module,
|
|
36
|
+
[module]
|
|
37
|
+
)
|
|
38
|
+
const connection = useValue<Connection>(async () => {
|
|
39
|
+
const _request = provideRequest(mod.getAlias(), mod.getPath())
|
|
40
|
+
Object.assign(_request, request)
|
|
41
|
+
return await ws(mod, _request)
|
|
42
|
+
}, [mod.getAlias(), request?.query?.[AUTH_QUERY]])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (connection != null) {
|
|
46
|
+
return () => {
|
|
47
|
+
void connection.close()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [connection])
|
|
51
|
+
|
|
52
|
+
return connection
|
|
53
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Config, Context } from '../types.js'
|
|
2
|
+
import type { Connection, EventMessage as EMessage } from '@owlmeans/socket'
|
|
3
|
+
import { createBasicConnection, MessageType } from '@owlmeans/socket'
|
|
4
|
+
|
|
5
|
+
export const makeConnection = <C extends Config = Config, T extends Context<C> = Context<C>>(
|
|
6
|
+
conn: WebSocket, _context: T
|
|
7
|
+
): Connection => {
|
|
8
|
+
const model = createBasicConnection()
|
|
9
|
+
|
|
10
|
+
model.send = async message => {
|
|
11
|
+
if (typeof message !== 'string') {
|
|
12
|
+
model.prepare?.(message)
|
|
13
|
+
}
|
|
14
|
+
conn.send(typeof message === 'string' ? message : JSON.stringify(message))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model.close = async () => {
|
|
18
|
+
// await closeHandler(new CloseEvent('close', { code: 1000, wasClean: true }))
|
|
19
|
+
conn.close()
|
|
20
|
+
// @TODO Make sure it trigger close observers
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
model.authenticate = async (_stage, _payload) => {
|
|
24
|
+
// @TODO Provide authentication flow logic here
|
|
25
|
+
return [] as any
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
model.prepare = (message, _isRequest) => {
|
|
29
|
+
// @TODO add sender / recipient metadata
|
|
30
|
+
message.dt = message.dt ?? Date.now()
|
|
31
|
+
return message
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const messageHandler = async (message: MessageEvent) => {
|
|
35
|
+
await model.receive(message.data)
|
|
36
|
+
}
|
|
37
|
+
const closeHandler = async (event: CloseEvent) => {
|
|
38
|
+
const msg: EMessage<{ code: number }> = {
|
|
39
|
+
type: MessageType.System,
|
|
40
|
+
event: 'close',
|
|
41
|
+
payload: { code: event.code }
|
|
42
|
+
}
|
|
43
|
+
if (model.prepare != null) {
|
|
44
|
+
model.prepare(msg)
|
|
45
|
+
}
|
|
46
|
+
await Promise.all(model.getListeners().map(async listener => listener(msg)))
|
|
47
|
+
conn.removeEventListener('message', messageHandler)
|
|
48
|
+
conn.removeEventListener('close', closeHandler)
|
|
49
|
+
}
|
|
50
|
+
conn.addEventListener('message', messageHandler)
|
|
51
|
+
conn.addEventListener('close', closeHandler)
|
|
52
|
+
|
|
53
|
+
return model
|
|
54
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
"../tsconfig.react.json",
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
8
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
9
|
+
"moduleResolution": "Bundler"
|
|
10
|
+
},
|
|
11
|
+
"exclude": [
|
|
12
|
+
"./dist/**/*",
|
|
13
|
+
"./build/**/*",
|
|
14
|
+
"./*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/helper.ts","./src/index.ts","./src/types.ts","./src/utils/connection.ts","./src/utils/index.ts"],"version":"5.6.3"}
|