[desktop] UI fix: integrate NgZone into Tauri WebSocket connection for better change detection
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import WebSocket, { Message } from '@tauri-apps/plugin-websocket';
|
import WebSocket, { ConnectionConfig, Message } from '@tauri-apps/plugin-websocket';
|
||||||
import { Subject, Observable } from 'rxjs';
|
import { Subject, Observable } from 'rxjs';
|
||||||
import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
|
||||||
const LOG_PREFIX = '[tauri_ws]';
|
const LOG_PREFIX = '[tauri_ws]';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ const LOG_PREFIX = '[tauri_ws]';
|
|||||||
*
|
*
|
||||||
* @template T - The type of messages sent and received through the WebSocket.
|
* @template T - The type of messages sent and received through the WebSocket.
|
||||||
* @param {WebSocketSubjectConfig<T>} opts - Configuration options for the WebSocket connection.
|
* @param {WebSocketSubjectConfig<T>} opts - Configuration options for the WebSocket connection.
|
||||||
|
* @param {NgZone} ngZone - Angular's NgZone to ensure change detection runs properly.
|
||||||
* @returns {WebSocketSubject<T>} - An RxJS WebSocketSubject-compatible object for interacting with the WebSocket.
|
* @returns {WebSocketSubject<T>} - An RxJS WebSocketSubject-compatible object for interacting with the WebSocket.
|
||||||
* @throws {Error} If the `serializer` or `deserializer` functions are not provided.
|
* @throws {Error} If the `serializer` or `deserializer` functions are not provided.
|
||||||
*
|
*
|
||||||
@@ -17,9 +19,9 @@ const LOG_PREFIX = '[tauri_ws]';
|
|||||||
* url: 'ws://example.com',
|
* url: 'ws://example.com',
|
||||||
* serializer: JSON.stringify,
|
* serializer: JSON.stringify,
|
||||||
* deserializer: JSON.parse,
|
* deserializer: JSON.parse,
|
||||||
* });
|
* }, ngZone);
|
||||||
*/
|
*/
|
||||||
export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>, ngZone: NgZone): WebSocketSubject<T> {
|
||||||
if (!opts.serializer) throw new Error(`${LOG_PREFIX} Messages Serializer not provided!`);
|
if (!opts.serializer) throw new Error(`${LOG_PREFIX} Messages Serializer not provided!`);
|
||||||
if (!opts.deserializer) throw new Error(`${LOG_PREFIX} Messages Deserializer not provided!`);
|
if (!opts.deserializer) throw new Error(`${LOG_PREFIX} Messages Deserializer not provided!`);
|
||||||
|
|
||||||
@@ -37,8 +39,12 @@ export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>): Web
|
|||||||
if (!descriptionToLog) return;
|
if (!descriptionToLog) return;
|
||||||
if (!error) error = new Error(descriptionToLog);
|
if (!error) error = new Error(descriptionToLog);
|
||||||
console.error(`${LOG_PREFIX} ${descriptionToLog}:`, error);
|
console.error(`${LOG_PREFIX} ${descriptionToLog}:`, error);
|
||||||
// This completes the observable and prevents further messages from being processed.
|
|
||||||
messageSubject.error(error);
|
// Run inside NgZone to ensure Angular detects this change
|
||||||
|
ngZone.run(() => {
|
||||||
|
// This completes the observable and prevents further messages from being processed.
|
||||||
|
messageSubject.error(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
@@ -67,24 +73,36 @@ export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>): Web
|
|||||||
console.error(`${LOG_PREFIX} Error serializing message:`, error);
|
console.error(`${LOG_PREFIX} Error serializing message:`, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Run outside NgZone for better performance during send operations
|
||||||
wsConnection.send(serializedMessage).catch((err: Error) => {
|
ngZone.runOutsideAngular(() => {
|
||||||
notifySubjectError('Error sending text message', err);
|
try {
|
||||||
});
|
wsConnection!.send(serializedMessage).catch((err: Error) => {
|
||||||
} catch (error) {
|
notifySubjectError('Error sending text message', err);
|
||||||
notifySubjectError('Error sending message', error);
|
});
|
||||||
}
|
} catch (error) {
|
||||||
|
notifySubjectError('Error sending message', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
complete: () => {
|
complete: () => {
|
||||||
if (wsConnection) {
|
if (wsConnection) {
|
||||||
console.log(`${LOG_PREFIX} Closing connection`);
|
console.log(`${LOG_PREFIX} Closing connection`);
|
||||||
opts.closingObserver?.next(undefined);
|
|
||||||
wsConnection.disconnect().catch((err: Error) => console.error(`${LOG_PREFIX} Error closing connection:`, err));
|
// Run inside NgZone to ensure Angular detects this change
|
||||||
wsConnection = null;
|
ngZone.run(() => {
|
||||||
|
if (opts.closingObserver?.next) {
|
||||||
|
opts.closingObserver.next(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConnection!.disconnect().catch((err: Error) => console.error(`${LOG_PREFIX} Error closing connection:`, err));
|
||||||
|
wsConnection = null;
|
||||||
|
messageSubject.complete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageSubject.complete();
|
||||||
}
|
}
|
||||||
messageSubject.complete();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// RxJS Observable methods required for compatibility
|
// RxJS Observable methods required for compatibility
|
||||||
@@ -97,98 +115,105 @@ export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>): Web
|
|||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
// Connect to WebSocket
|
// Connect to WebSocket
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
console.log(`${LOG_PREFIX} Connecting to WebSocket:`, opts.url);
|
||||||
const connectOptions: Record<string, any> = {};
|
|
||||||
console.log(`${LOG_PREFIX} Connecting to WebSocket:`, opts.url, connectOptions);
|
// Connect outside of Angular zone for better performance
|
||||||
WebSocket.connect(opts.url, connectOptions)
|
ngZone.runOutsideAngular(() => {
|
||||||
.then((ws) => {
|
WebSocket.connect(opts.url)
|
||||||
wsConnection = ws;
|
.then((ws) => {
|
||||||
console.log(`${LOG_PREFIX} Connection established`);
|
wsConnection = ws;
|
||||||
|
console.log(`${LOG_PREFIX} Connection established`);
|
||||||
// Create a mock Event for the openObserver
|
|
||||||
if (opts.openObserver) {
|
// Run inside NgZone to ensure Angular detects this connection event
|
||||||
const mockEvent = new Event('open') as Event;
|
ngZone.run(() => {
|
||||||
opts.openObserver.next(mockEvent);
|
// Create a mock Event for the openObserver
|
||||||
}
|
if (opts.openObserver) {
|
||||||
|
const mockEvent = new Event('open') as Event;
|
||||||
// Send any pending messages
|
opts.openObserver.next(mockEvent);
|
||||||
while (pendingMessages.length > 0) {
|
}
|
||||||
const message = pendingMessages.shift();
|
|
||||||
if (message) webSocketSubject.next(message);
|
// Send any pending messages
|
||||||
}
|
while (pendingMessages.length > 0) {
|
||||||
|
const message = pendingMessages.shift();
|
||||||
try {
|
if (message) webSocketSubject.next(message);
|
||||||
// Add a single listener for ALL message types according to Tauri WebSocket API
|
|
||||||
// The addListener method takes a single callback function that receives messages
|
|
||||||
ws.addListener((message: Message) => {
|
|
||||||
try {
|
|
||||||
// Handle different message types from Tauri
|
|
||||||
switch (message.type) {
|
|
||||||
case 'Text':
|
|
||||||
const textData = message.data as string;
|
|
||||||
try {
|
|
||||||
const deserializedMessage = deserializer({ data: textData } as any) ;
|
|
||||||
messageSubject.next(deserializedMessage);
|
|
||||||
} catch (err) {
|
|
||||||
notifySubjectError('Error deserializing text message', err);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Binary':
|
|
||||||
const binaryData = message.data as number[];
|
|
||||||
try {
|
|
||||||
const uint8Array = new Uint8Array(binaryData);
|
|
||||||
const buffer = uint8Array.buffer;
|
|
||||||
const deserializedMessage = deserializer({ data: buffer } as any) ;
|
|
||||||
messageSubject.next(deserializedMessage);
|
|
||||||
} catch (err) {
|
|
||||||
notifySubjectError('Error deserializing binary message', err);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Close':
|
|
||||||
// Handle close message
|
|
||||||
const closeData = message.data as { code: number; reason: string } | null;
|
|
||||||
console.log(`${LOG_PREFIX} Connection closed by server`, closeData);
|
|
||||||
|
|
||||||
if (opts.closeObserver) {
|
|
||||||
const closeEvent = {
|
|
||||||
code: closeData?.code || 1000,
|
|
||||||
reason: closeData?.reason || '',
|
|
||||||
wasClean: true,
|
|
||||||
type: 'close',
|
|
||||||
target: null
|
|
||||||
} as unknown as CloseEvent;
|
|
||||||
|
|
||||||
opts.closeObserver.next(closeEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
messageSubject.complete();
|
|
||||||
wsConnection = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Ping':
|
|
||||||
console.log(`${LOG_PREFIX} Received ping`);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Pong':
|
|
||||||
console.log(`${LOG_PREFIX} Received pong`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`${LOG_PREFIX} Error processing message:`, error);
|
|
||||||
// Don't error the subject on message processing errors to keep connection alive
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`${LOG_PREFIX} Listener added successfully`);
|
|
||||||
|
|
||||||
} catch (error) {
|
try {
|
||||||
notifySubjectError('Error adding message listener', error);
|
// Add a single listener for ALL message types according to Tauri WebSocket API
|
||||||
}
|
ws.addListener((message: Message) => {
|
||||||
})
|
// Process message inside ngZone to trigger change detection
|
||||||
.catch((error: Error) => {
|
ngZone.run(() => {
|
||||||
notifySubjectError('Connection failed', error);
|
try {
|
||||||
|
// Handle different message types from Tauri
|
||||||
|
switch (message.type) {
|
||||||
|
case 'Text':
|
||||||
|
const textData = message.data as string;
|
||||||
|
try {
|
||||||
|
const deserializedMessage = deserializer({ data: textData } as any);
|
||||||
|
messageSubject.next(deserializedMessage);
|
||||||
|
} catch (err) {
|
||||||
|
notifySubjectError('Error deserializing text message', err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Binary':
|
||||||
|
const binaryData = message.data as number[];
|
||||||
|
try {
|
||||||
|
const uint8Array = new Uint8Array(binaryData);
|
||||||
|
const buffer = uint8Array.buffer;
|
||||||
|
const deserializedMessage = deserializer({ data: buffer } as any);
|
||||||
|
messageSubject.next(deserializedMessage);
|
||||||
|
} catch (err) {
|
||||||
|
notifySubjectError('Error deserializing binary message', err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Close':
|
||||||
|
// Handle close message
|
||||||
|
const closeData = message.data as { code: number; reason: string } | null;
|
||||||
|
console.log(`${LOG_PREFIX} Connection closed by server`, closeData);
|
||||||
|
|
||||||
|
if (opts.closeObserver) {
|
||||||
|
const closeEvent = {
|
||||||
|
code: closeData?.code || 1000,
|
||||||
|
reason: closeData?.reason || '',
|
||||||
|
wasClean: true,
|
||||||
|
type: 'close',
|
||||||
|
target: null
|
||||||
|
} as unknown as CloseEvent;
|
||||||
|
|
||||||
|
opts.closeObserver.next(closeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSubject.complete();
|
||||||
|
wsConnection = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Ping':
|
||||||
|
console.log(`${LOG_PREFIX} Received ping`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Pong':
|
||||||
|
console.log(`${LOG_PREFIX} Received pong`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${LOG_PREFIX} Error processing message:`, error);
|
||||||
|
// Don't error the subject on message processing errors to keep connection alive
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${LOG_PREFIX} Listener added successfully`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
notifySubjectError('Error adding message listener', error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
notifySubjectError('Connection failed', error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cast to WebSocketSubject<T>
|
// Cast to WebSocketSubject<T>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||||
import { createTauriWsConnection } from './platform-specific/tauri/tauri-websocket-subject';
|
import { createTauriWsConnection } from './platform-specific/tauri/tauri-websocket-subject';
|
||||||
import { IsTauriEnvironment } from './platform-specific/utils';
|
import { IsTauriEnvironment } from './platform-specific/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebsocketService {
|
export class WebsocketService {
|
||||||
constructor() { }
|
constructor(private ngZone: NgZone) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* createConnection creates a new websocket connection using opts.
|
* createConnection creates a new websocket connection using opts.
|
||||||
@@ -13,9 +13,9 @@ export class WebsocketService {
|
|||||||
* @param opts Options for the websocket connection.
|
* @param opts Options for the websocket connection.
|
||||||
*/
|
*/
|
||||||
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
||||||
if (IsTauriEnvironment()) {
|
if (IsTauriEnvironment()) {
|
||||||
console.log('[portmaster-api] Running under Tauri - Using Tauri WebSocket');
|
console.log('[portmaster-api] Running under Tauri - Using Tauri WebSocket');
|
||||||
return createTauriWsConnection<T>(opts);
|
return createTauriWsConnection<T>(opts, this.ngZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[portmaster-api] Running in browser - Using RxJS WebSocket');
|
console.log('[portmaster-api] Running in browser - Using RxJS WebSocket');
|
||||||
|
|||||||
Reference in New Issue
Block a user