oxigdal_websocket/client_sdk/
javascript.rs1use crate::client_sdk::ClientSdkConfig;
4
5pub 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
366pub 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}