@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 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"}
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export type * from './types.js';
2
+ export * from './helper.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from './helper.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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,2 @@
1
+ export * from './connection.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from './connection.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1,3 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './helper.js'
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+
2
+ import type { ClientConfig } from '@owlmeans/client-context'
3
+ import type { ClientContext } from '@owlmeans/client'
4
+
5
+ export interface Config extends ClientConfig { }
6
+
7
+ export interface Context<C extends Config = Config> extends ClientContext<C> { }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export * from './connection.js'
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"}