Skip to main content

oxigdal_websocket/client_sdk/
javascript.rs

1//! JavaScript client SDK generation
2
3use crate::client_sdk::ClientSdkConfig;
4
5/// Generate JavaScript client SDK
6pub fn generate_javascript_client(config: &ClientSdkConfig) -> String {
7    let reconnection_code = if config.enable_reconnection {
8        format!(
9            r#"
10    reconnect() {{
11        if (this.reconnectAttempts >= {max_attempts}) {{
12            console.error('Max reconnection attempts reached');
13            this.emit('maxReconnectAttemptsReached');
14            return;
15        }}
16
17        this.reconnectAttempts++;
18        const delay = {delay} * Math.pow(2, this.reconnectAttempts - 1);
19
20        console.log(`Reconnecting in ${{delay}}ms (attempt ${{this.reconnectAttempts}})`);
21
22        setTimeout(() => {{
23            this.connect(this.url);
24        }}, delay);
25    }}"#,
26            max_attempts = config.max_reconnection_attempts,
27            delay = config.reconnection_delay_ms
28        )
29    } else {
30        "    // Reconnection disabled".to_string()
31    };
32
33    let caching_code = if config.enable_caching {
34        r#"
35    initCache() {
36        this.cache = {
37            tiles: new Map(),
38            features: new Map(),
39        };
40    }
41
42    getCachedTile(z, x, y) {
43        const key = `${z}/${x}/${y}`;
44        return this.cache.tiles.get(key);
45    }
46
47    cacheTile(z, x, y, data) {
48        const key = `${z}/${x}/${y}`;
49        this.cache.tiles.set(key, data);
50    }
51
52    getCachedFeature(layer, id) {
53        const key = `${layer}:${id}`;
54        return this.cache.features.get(key);
55    }
56
57    cacheFeature(layer, id, feature) {
58        const key = `${layer}:${id}`;
59        this.cache.features.set(key, feature);
60    }
61
62    clearCache() {
63        this.cache.tiles.clear();
64        this.cache.features.clear();
65    }"#
66    } else {
67        "    // Caching disabled"
68    };
69
70    format!(
71        r#"/**
72 * OxiGDAL WebSocket Client
73 *
74 * A JavaScript client for connecting to OxiGDAL WebSocket server
75 * with support for real-time geospatial data updates.
76 */
77
78class OxiGDALWebSocketClient {{
79    constructor(options = {{}}) {{
80        this.url = null;
81        this.ws = null;
82        this.connected = false;
83        this.reconnectAttempts = 0;
84        this.maxReconnectAttempts = {max_reconnect};
85        this.reconnectDelay = {reconnect_delay};
86        this.subscriptions = new Map();
87        this.rooms = new Set();
88        this.messageHandlers = new Map();
89        this.eventEmitter = new EventTarget();
90
91        {init_cache}
92    }}
93
94    /**
95     * Connect to the WebSocket server
96     */
97    connect(url) {{
98        this.url = url;
99
100        try {{
101            this.ws = new WebSocket(url);
102
103            this.ws.onopen = () => {{
104                console.log('WebSocket connected');
105                this.connected = true;
106                this.reconnectAttempts = 0;
107                this.emit('connected');
108            }};
109
110            this.ws.onmessage = (event) => {{
111                this.handleMessage(event.data);
112            }};
113
114            this.ws.onerror = (error) => {{
115                console.error('WebSocket error:', error);
116                this.emit('error', error);
117            }};
118
119            this.ws.onclose = () => {{
120                console.log('WebSocket disconnected');
121                this.connected = false;
122                this.emit('disconnected');
123
124                if (this.reconnectAttempts < this.maxReconnectAttempts) {{
125                    this.reconnect();
126                }}
127            }};
128        }} catch (error) {{
129            console.error('Failed to connect:', error);
130            this.emit('error', error);
131        }}
132    }}
133
134    /**
135     * Disconnect from the server
136     */
137    disconnect() {{
138        if (this.ws) {{
139            this.ws.close();
140            this.ws = null;
141        }}
142        this.connected = false;
143    }}
144
145    /**
146     * Send a message to the server
147     */
148    send(message) {{
149        if (!this.connected || !this.ws) {{
150            console.error('Not connected');
151            return false;
152        }}
153
154        try {{
155            const data = JSON.stringify(message);
156            this.ws.send(data);
157            return true;
158        }} catch (error) {{
159            console.error('Failed to send message:', error);
160            return false;
161        }}
162    }}
163
164    /**
165     * Subscribe to a topic
166     */
167    subscribe(topic, handler) {{
168        if (!this.subscriptions.has(topic)) {{
169            this.subscriptions.set(topic, new Set());
170        }}
171        this.subscriptions.get(topic).add(handler);
172
173        // Send subscribe message to server
174        this.send({{
175            msg_type: 'Subscribe',
176            payload: {{
177                Subscribe: {{
178                    topic: topic,
179                    filter: null
180                }}
181            }}
182        }});
183
184        return () => this.unsubscribe(topic, handler);
185    }}
186
187    /**
188     * Unsubscribe from a topic
189     */
190    unsubscribe(topic, handler) {{
191        if (this.subscriptions.has(topic)) {{
192            this.subscriptions.get(topic).delete(handler);
193
194            if (this.subscriptions.get(topic).size === 0) {{
195                this.subscriptions.delete(topic);
196
197                // Send unsubscribe message to server
198                this.send({{
199                    msg_type: 'Unsubscribe',
200                    payload: {{
201                        Subscribe: {{
202                            topic: topic,
203                            filter: null
204                        }}
205                    }}
206                }});
207            }}
208        }}
209    }}
210
211    /**
212     * Join a room
213     */
214    joinRoom(roomName) {{
215        this.rooms.add(roomName);
216
217        this.send({{
218            msg_type: 'JoinRoom',
219            payload: {{
220                Room: {{
221                    room: roomName
222                }}
223            }}
224        }});
225    }}
226
227    /**
228     * Leave a room
229     */
230    leaveRoom(roomName) {{
231        this.rooms.delete(roomName);
232
233        this.send({{
234            msg_type: 'LeaveRoom',
235            payload: {{
236                Room: {{
237                    room: roomName
238                }}
239            }}
240        }});
241    }}
242
243    /**
244     * Handle incoming message
245     */
246    handleMessage(data) {{
247        try {{
248            const message = JSON.parse(data);
249
250            // Handle based on message type
251            switch (message.msg_type) {{
252                case 'TileUpdate':
253                    this.handleTileUpdate(message);
254                    break;
255                case 'FeatureUpdate':
256                    this.handleFeatureUpdate(message);
257                    break;
258                case 'ChangeStream':
259                    this.handleChangeStream(message);
260                    break;
261                case 'Pong':
262                    this.emit('pong');
263                    break;
264                default:
265                    this.emit('message', message);
266            }}
267        }} catch (error) {{
268            console.error('Failed to handle message:', error);
269        }}
270    }}
271
272    /**
273     * Handle tile update
274     */
275    handleTileUpdate(message) {{
276        const tile = message.payload.TileData;
277
278        {cache_tile}
279
280        this.emit('tileUpdate', tile);
281    }}
282
283    /**
284     * Handle feature update
285     */
286    handleFeatureUpdate(message) {{
287        const feature = message.payload.FeatureData;
288
289        {cache_feature}
290
291        this.emit('featureUpdate', feature);
292    }}
293
294    /**
295     * Handle change stream event
296     */
297    handleChangeStream(message) {{
298        const change = message.payload.ChangeEvent;
299        this.emit('changeStream', change);
300    }}
301
302    /**
303     * Send ping to server
304     */
305    ping() {{
306        this.send({{
307            msg_type: 'Ping',
308            payload: 'Empty'
309        }});
310    }}
311
312    /**
313     * Emit an event
314     */
315    emit(eventName, data) {{
316        const event = new CustomEvent(eventName, {{ detail: data }});
317        this.eventEmitter.dispatchEvent(event);
318    }}
319
320    /**
321     * Add event listener
322     */
323    on(eventName, handler) {{
324        this.eventEmitter.addEventListener(eventName, (e) => handler(e.detail));
325    }}
326
327    /**
328     * Remove event listener
329     */
330    off(eventName, handler) {{
331        this.eventEmitter.removeEventListener(eventName, handler);
332    }}
333
334{reconnection_code}
335
336{caching_code}
337}}
338
339// Export for use in Node.js and browsers
340if (typeof module !== 'undefined' && module.exports) {{
341    module.exports = OxiGDALWebSocketClient;
342}}
343"#,
344        max_reconnect = config.max_reconnection_attempts,
345        reconnect_delay = config.reconnection_delay_ms,
346        init_cache = if config.enable_caching {
347            "this.initCache();"
348        } else {
349            ""
350        },
351        cache_tile = if config.enable_caching {
352            "this.cacheTile(tile.z, tile.x, tile.y, tile);"
353        } else {
354            ""
355        },
356        cache_feature = if config.enable_caching {
357            "this.cacheFeature(feature.layer, feature.id, feature);"
358        } else {
359            ""
360        },
361        reconnection_code = reconnection_code,
362        caching_code = caching_code,
363    )
364}
365
366/// Generate TypeScript definitions
367pub fn generate_typescript_definitions() -> String {
368    r#"/**
369 * OxiGDAL WebSocket Client TypeScript Definitions
370 */
371
372export interface OxiGDALClientOptions {
373    reconnectAttempts?: number;
374    reconnectDelay?: number;
375}
376
377export interface Message {
378    id: string;
379    msg_type: MessageType;
380    timestamp: number;
381    payload: Payload;
382    correlation_id?: string;
383}
384
385export type MessageType =
386    | 'Ping'
387    | 'Pong'
388    | 'Subscribe'
389    | 'Unsubscribe'
390    | 'Publish'
391    | 'Data'
392    | 'TileUpdate'
393    | 'FeatureUpdate'
394    | 'ChangeStream'
395    | 'Error'
396    | 'Ack'
397    | 'JoinRoom'
398    | 'LeaveRoom'
399    | 'Broadcast'
400    | 'SystemEvent';
401
402export type Payload = any; // Can be refined based on message type
403
404export interface TileData {
405    z: number;
406    x: number;
407    y: number;
408    data: Uint8Array;
409    format: string;
410    delta?: Uint8Array;
411}
412
413export interface FeatureData {
414    id: string;
415    layer: string;
416    feature: GeoJSON.Feature;
417    change_type: ChangeType;
418}
419
420export type ChangeType = 'Created' | 'Updated' | 'Deleted';
421
422export interface ChangeEvent {
423    change_id: number;
424    collection: string;
425    change_type: ChangeType;
426    document_id: string;
427    data?: any;
428}
429
430export declare class OxiGDALWebSocketClient {
431    constructor(options?: OxiGDALClientOptions);
432
433    connect(url: string): void;
434    disconnect(): void;
435    send(message: Message): boolean;
436
437    subscribe(topic: string, handler: (data: any) => void): () => void;
438    unsubscribe(topic: string, handler: (data: any) => void): void;
439
440    joinRoom(roomName: string): void;
441    leaveRoom(roomName: string): void;
442
443    ping(): void;
444
445    on(eventName: string, handler: (data: any) => void): void;
446    off(eventName: string, handler: (data: any) => void): void;
447
448    getCachedTile?(z: number, x: number, y: number): TileData | undefined;
449    cacheTile?(z: number, x: number, y: number, data: TileData): void;
450    getCachedFeature?(layer: string, id: string): FeatureData | undefined;
451    cacheFeature?(layer: string, id: string, feature: FeatureData): void;
452    clearCache?(): void;
453}
454
455export default OxiGDALWebSocketClient;
456"#
457    .to_string()
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_generate_javascript_client() {
466        let config = ClientSdkConfig::default();
467        let js_code = generate_javascript_client(&config);
468
469        assert!(js_code.contains("OxiGDALWebSocketClient"));
470        assert!(js_code.contains("connect"));
471        assert!(js_code.contains("subscribe"));
472        assert!(js_code.contains("joinRoom"));
473    }
474
475    #[test]
476    fn test_generate_javascript_client_no_reconnection() {
477        let config = ClientSdkConfig {
478            enable_reconnection: false,
479            ..Default::default()
480        };
481        let js_code = generate_javascript_client(&config);
482
483        assert!(js_code.contains("Reconnection disabled"));
484    }
485
486    #[test]
487    fn test_generate_javascript_client_no_caching() {
488        let config = ClientSdkConfig {
489            enable_caching: false,
490            ..Default::default()
491        };
492        let js_code = generate_javascript_client(&config);
493
494        assert!(js_code.contains("Caching disabled"));
495    }
496
497    #[test]
498    fn test_generate_typescript_definitions() {
499        let ts_defs = generate_typescript_definitions();
500
501        assert!(ts_defs.contains("OxiGDALWebSocketClient"));
502        assert!(ts_defs.contains("MessageType"));
503        assert!(ts_defs.contains("TileData"));
504        assert!(ts_defs.contains("FeatureData"));
505    }
506}