Initial public release

This commit is contained in:
Bohdan Triapitsyn
2025-12-07 19:32:53 +02:00
commit 4b2edf7318
319 changed files with 81600 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
import { safeInvoke, cleanupAllTauriCallbacks } from './tauriCallbackManager';
type ServerInfo = {
server_port: number;
opencode_port?: number | null;
api_prefix?: string | null;
cli_available?: boolean;
};
declare global {
interface Window {
__OPENCHAMBER_DESKTOP_SERVER__?: {
origin: string;
opencodePort: number | null;
apiPrefix: string;
cliAvailable: boolean;
};
}
}
let bridgePromise: Promise<void> | null = null;
export function initializeDesktopBridge(): Promise<void> {
if (!bridgePromise) {
bridgePromise = setupBridge();
}
return bridgePromise;
}
async function setupBridge(): Promise<void> {
try {
const info = await safeInvoke<ServerInfo>('desktop_server_info', {}, {
timeout: 10000,
onCancel: () => {
console.warn('[Bridge] Server info request timed out');
}
});
const origin = `http://127.0.0.1:${info.server_port}`;
window.__OPENCHAMBER_DESKTOP_SERVER__ = {
origin,
opencodePort: info.opencode_port ?? null,
apiPrefix: info.api_prefix ?? '',
cliAvailable: info.cli_available ?? false,
};
patchFetch(origin);
patchEventSource(origin);
const cleanupDevtools = registerDevtoolsShortcut();
if (typeof window !== 'undefined') {
(window as { __openchamberCleanup?: () => void }).__openchamberCleanup = () => {
cleanupDevtools();
};
}
} catch (error) {
console.error('[bridge] Failed to initialize bridge:', error);
if (typeof window !== 'undefined' && (window as { __openchamberCleanup?: () => void }).__openchamberCleanup) {
try {
(window as { __openchamberCleanup?: () => void }).__openchamberCleanup?.();
} catch (cleanupError) {
console.warn('[bridge] Cleanup during failed initialization failed:', cleanupError);
}
delete (window as { __openchamberCleanup?: () => void }).__openchamberCleanup;
}
cleanupAllTauriCallbacks();
throw error;
}
}
function patchFetch(origin: string) {
const originalFetch = window.fetch.bind(window);
const rewrite = (value: string): string => {
if (value.startsWith('http://') || value.startsWith('https://')) {
return value;
}
if (value.startsWith('//')) {
return `http:${value}`;
}
if (value.startsWith('/')) {
return `${origin}${value}`;
}
return value;
};
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
if (typeof input === 'string') {
return originalFetch(rewrite(input), init);
}
if (input instanceof Request) {
const rewritten = rewrite(input.url);
if (rewritten === input.url) {
return originalFetch(input, init);
}
const cloned = new Request(rewritten, input);
return originalFetch(cloned, init);
}
if (input instanceof URL) {
return originalFetch(rewrite(input.toString()), init);
}
return originalFetch(input, init);
};
}
function patchEventSource(origin: string) {
if (typeof window.EventSource === 'undefined') {
return;
}
const OriginalEventSource = window.EventSource;
class DesktopEventSource extends OriginalEventSource {
constructor(url: string | URL, eventSourceInit?: EventSourceInit) {
const normalized = typeof url === 'string' ? url : url.toString();
super(normalized.startsWith('/') ? `${origin}${normalized}` : normalized, eventSourceInit);
}
}
Object.defineProperty(DesktopEventSource, 'name', { value: 'DesktopEventSource' });
Object.setPrototypeOf(DesktopEventSource.prototype, OriginalEventSource.prototype);
Object.setPrototypeOf(DesktopEventSource, OriginalEventSource);
window.EventSource = DesktopEventSource as unknown as typeof EventSource;
}
function registerDevtoolsShortcut() {
const handler = (event: KeyboardEvent) => {
const key = event.key?.toLowerCase();
if ((event.metaKey || event.ctrlKey) && event.altKey && key === 'i') {
event.preventDefault();
const devtoolsPromise = safeInvoke('desktop_open_devtools', {}, {
timeout: 2000,
onCancel: () => {
console.warn('[Bridge] Devtools invocation timed out');
}
});
devtoolsPromise.catch(() => {
});
}
};
window.addEventListener('keydown', handler);
return () => {
window.removeEventListener('keydown', handler);
};
}
@@ -0,0 +1,308 @@
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
interface PendingCallback {
id: string;
timestamp: number;
type: 'invoke' | 'listen';
cleanup?: () => void;
timeout?: NodeJS.Timeout;
}
interface CallbackManagerConfig {
maxCallbackAge?: number;
cleanupInterval?: number;
invokeTimeout?: number;
listenTimeout?: number;
}
class TauriCallbackManager {
private callbacks = new Map<string, PendingCallback>();
private isShuttingDown = false;
private cleanupTimer?: NodeJS.Timeout;
private config: Required<CallbackManagerConfig>;
private windowUnloadHandler?: () => void;
constructor(config: CallbackManagerConfig = {}) {
this.config = {
maxCallbackAge: 30000,
cleanupInterval: 5000,
invokeTimeout: 10000,
listenTimeout: 30000,
...config,
};
this.setupWindowUnloadHandler();
this.startCleanupTimer();
}
register(callback: Omit<PendingCallback, 'timestamp'>): string {
if (this.isShuttingDown) {
console.warn('[TauriCallbackManager] Attempted to register callback during shutdown');
return callback.id;
}
const fullCallback: PendingCallback = {
...callback,
timestamp: Date.now(),
};
this.callbacks.set(callback.id, fullCallback);
if (callback.type === 'listen' && this.config.listenTimeout > 0) {
const timeout = setTimeout(() => {
this.cleanupCallback(callback.id, 'timeout');
}, this.config.listenTimeout);
fullCallback.timeout = timeout;
}
return callback.id;
}
unregister(callbackId: string): void {
const callback = this.callbacks.get(callbackId);
if (!callback) {
return;
}
if (callback.timeout) {
clearTimeout(callback.timeout);
}
if (callback.cleanup) {
try {
callback.cleanup();
} catch (error) {
console.warn('[TauriCallbackManager] Cleanup function failed:', error);
}
}
this.callbacks.delete(callbackId);
}
private cleanupCallback(callbackId: string, reason: 'timeout' | 'shutdown' | 'expired'): void {
const callback = this.callbacks.get(callbackId);
if (!callback) {
return;
}
if (reason === 'expired') {
console.warn(`[TauriCallbackManager] Callback ${callbackId} expired and was cleaned up`);
}
this.unregister(callbackId);
}
cleanupAll(): void {
this.isShuttingDown = true;
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
const callbackIds = Array.from(this.callbacks.keys());
callbackIds.forEach(id => this.cleanupCallback(id, 'shutdown'));
this.callbacks.clear();
}
private startCleanupTimer(): void {
this.cleanupTimer = setInterval(() => {
if (this.isShuttingDown) {
return;
}
const now = Date.now();
const expiredCallbacks: string[] = [];
this.callbacks.forEach((callback, id) => {
const age = now - callback.timestamp;
if (age > this.config.maxCallbackAge) {
expiredCallbacks.push(id);
}
});
expiredCallbacks.forEach(id => this.cleanupCallback(id, 'expired'));
}, this.config.cleanupInterval);
}
private setupWindowUnloadHandler(): void {
if (typeof window === 'undefined') {
return;
}
this.windowUnloadHandler = () => {
console.info('[TauriCallbackManager] Window unloading, cleaning up callbacks...');
this.cleanupAll();
};
window.addEventListener('beforeunload', this.windowUnloadHandler);
window.addEventListener('pagehide', this.windowUnloadHandler);
}
removeWindowHandlers(): void {
if (this.windowUnloadHandler && typeof window !== 'undefined') {
window.removeEventListener('beforeunload', this.windowUnloadHandler);
window.removeEventListener('pagehide', this.windowUnloadHandler);
this.windowUnloadHandler = undefined;
}
}
getStats(): { total: number; invoke: number; listen: number } {
const stats = { total: 0, invoke: 0, listen: 0 };
this.callbacks.forEach(callback => {
stats.total++;
stats[callback.type]++;
});
return stats;
}
}
let globalCallbackManager: TauriCallbackManager | null = null;
export function getTauriCallbackManager(config?: CallbackManagerConfig): TauriCallbackManager {
if (!globalCallbackManager) {
globalCallbackManager = new TauriCallbackManager(config);
}
return globalCallbackManager;
}
export async function safeInvoke<T>(
command: string,
args?: Record<string, unknown>,
options?: {
timeout?: number;
onCancel?: () => void;
}
): Promise<T> {
const manager = getTauriCallbackManager();
const callbackId = `invoke:${command}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
let timeoutHandle: NodeJS.Timeout | undefined;
let settled = false;
manager.register({
id: callbackId,
type: 'invoke',
});
const clearAndUnregister = () => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = undefined;
}
manager.unregister(callbackId);
};
if (!options?.timeout || options.timeout <= 0) {
try {
const result = await invoke<T>(command, args);
clearAndUnregister();
return result;
} catch (error) {
clearAndUnregister();
throw error;
}
}
return new Promise<T>((resolve, reject) => {
timeoutHandle = setTimeout(() => {
if (settled) {
return;
}
settled = true;
console.warn(`[safeInvoke] Command ${command} timed out after ${options.timeout}ms`);
try {
options.onCancel?.();
} catch (error) {
console.warn('[safeInvoke] onCancel handler threw:', error);
}
clearAndUnregister();
reject(new Error(`Command ${command} timed out after ${options.timeout}ms`));
}, options.timeout);
invoke<T>(command, args)
.then((result) => {
if (settled) {
return;
}
settled = true;
clearAndUnregister();
resolve(result);
})
.catch((error) => {
if (settled) {
return;
}
settled = true;
clearAndUnregister();
reject(error);
});
});
}
export async function safeListen<T>(
event: string,
handler: (event: { payload: T }) => void,
options?: {
timeout?: number;
onCancel?: () => void;
}
): Promise<UnlistenFn> {
const manager = getTauriCallbackManager();
const callbackId = `listen:${event}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
try {
manager.register({
id: callbackId,
type: 'listen',
cleanup: options?.onCancel,
});
const unlisten = await listen<T>(event, (event) => {
const currentManager = getTauriCallbackManager();
if (currentManager.getStats().total === 0) {
return;
}
try {
handler(event);
} catch (error) {
console.error(`[safeListen] Handler error for event ${event}:`, error);
}
});
const enhancedUnlisten = () => {
try {
unlisten();
} catch (error) {
console.warn(`[safeListen] Failed to unlisten from ${event}:`, error);
}
manager.unregister(callbackId);
};
return enhancedUnlisten;
} catch (error) {
manager.unregister(callbackId);
throw error;
}
}
export function cleanupAllTauriCallbacks(): void {
if (globalCallbackManager) {
globalCallbackManager.cleanupAll();
globalCallbackManager.removeWindowHandlers();
globalCallbackManager = null;
}
}