1/**
2 * Omnibox Message Bridge
3 *
4 * TypeScript interface for Go-JavaScript communication
5 */
6
7import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, Favorite, SearchShortcut } from "./types";
8import { omniboxStore } from "./stores.svelte.ts";
9
10export class OmniboxBridge implements OmniboxMessageBridge {
11 // Forward messages via CustomEvent to main world bridge
12 // The main world will forward to webkit.messageHandlers.dumber
13 postMessage(msg: OmniboxMessage): void {
14 console.log("📤 [DEBUG] Posting message to backend via bridge:", msg);
15 try {
16 // Dispatch CustomEvent that main-world bridge will listen to
17 document.dispatchEvent(new CustomEvent("dumber:isolated-message", {
18 detail: { payload: msg }
19 }));
20 console.log("✅ [DEBUG] Dispatched isolated message event");
21 } catch (e) {
22 console.error("❌ [DEBUG] Failed to dispatch message:", e);
23 // Fallback navigation if dispatch fails
24 if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
25 try {
26 window.location.href = msg.url;
27 } catch (navError) {
28 console.error("Fallback navigation failed:", navError);
29 }
30 }
31 }
32 }
33
34 /**
35 * Update suggestions from Go backend
36 */
37 setSuggestions(suggestions: Suggestion[]): void {
38 console.log("📝 [DEBUG] Received suggestions from backend:", suggestions);
39 omniboxStore.updateSuggestions(suggestions);
40 }
41
42 /**
43 * Update search shortcuts from Go backend
44 */
45 setSearchShortcuts(shortcuts: Record<string, SearchShortcut>): void {
46 console.log("📝 [DEBUG] Received search shortcuts from backend:", shortcuts);
47 omniboxStore.updateSearchShortcuts(shortcuts);
48 }
49
50 /**
51 * Handle navigation request
52 */
53 navigate(url: string): void {
54 console.log("🚀 Omnibox navigate called with:", url);
55 this.postMessage({ type: "navigate", url });
56 }
57
58 /**
59 * Handle search query
60 */
61 query(searchTerm: string, limit?: number): void {
62 const q = searchTerm ?? "";
63 const lim = limit || omniboxStore.config.defaultLimit;
64 console.log("🔍 [DEBUG] Sending query to backend:", { q, limit: lim });
65 // Send to native handler; Go will compute suggestions and call setSuggestions
66 this.postMessage({ type: "query", q, limit: lim });
67 }
68
69 /**
70 * Fetch search shortcuts from backend via messaging bridge
71 */
72 async fetchSearchShortcuts(): Promise<void> {
73 return new Promise((resolve, reject) => {
74 // Set up one-time response handler
75 const originalCallback = (window as any).__dumber_search_shortcuts;
76 (window as any).__dumber_search_shortcuts = (data: unknown) => {
77 try {
78 // Type guard
79 if (typeof data !== "object" || data === null) {
80 reject(new Error("Invalid shortcuts data"));
81 return;
82 }
83 const dataObj = data as Record<string, unknown>;
84 // Normalize to match our SearchShortcut type
85 const normalized: Record<string, SearchShortcut> = {};
86 for (const [key, value] of Object.entries(dataObj)) {
87 const v = value as Record<string, unknown>;
88 normalized[key] = {
89 url: (v.url ?? v.URL ?? "") as string,
90 description: (v.description ?? v.Description ?? "") as string,
91 };
92 }
93 this.setSearchShortcuts(normalized);
94
95 // Restore original callback if it existed
96 if (originalCallback) {
97 (window as any).__dumber_search_shortcuts = originalCallback;
98 }
99
100 resolve();
101 } catch (error) {
102 console.error("Failed to process search shortcuts:", error);
103 reject(error);
104 }
105 };
106
107 // Send message to Go backend
108 const bridge = window.webkit?.messageHandlers?.dumber;
109 if (bridge && typeof bridge.postMessage === "function") {
110 bridge.postMessage(
111 JSON.stringify({
112 type: "get_search_shortcuts",
113 }),
114 );
115 } else {
116 reject(new Error("WebKit message handler not available"));
117 }
118 });
119 }
120
121 /**
122 * Update favorites from Go backend
123 */
124 setFavorites(favorites: Favorite[]): void {
125 console.log("📝 [DEBUG] Received favorites from backend:", favorites);
126 omniboxStore.updateFavorites(favorites);
127 }
128
129 /**
130 * Fetch favorites from backend via messaging bridge
131 */
132 async fetchFavorites(): Promise<void> {
133 return new Promise((resolve, reject) => {
134 // Set up one-time event listener for favorites response
135 const handleFavorites = ((event: CustomEvent) => {
136 try {
137 const { favorites } = event.detail;
138 if (!Array.isArray(favorites)) {
139 reject(new Error("Invalid favorites data"));
140 return;
141 }
142 console.log("📝 [DEBUG] Received favorites via CustomEvent:", favorites);
143 this.setFavorites(favorites as Favorite[]);
144
145 // Clean up event listener
146 document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
147
148 resolve();
149 } catch (error) {
150 console.error("Failed to process favorites:", error);
151 document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
152 reject(error);
153 }
154 }) as EventListener;
155
156 // Listen for favorites event from main world bridge
157 document.addEventListener("dumber:favorites", handleFavorites as EventListener);
158
159 // Send message to Go backend via CustomEvent bridge (like other omnibox messages)
160 this.postMessage({ type: "get_favorites" });
161
162 // Timeout after 5 seconds
163 setTimeout(() => {
164 document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
165 reject(new Error("Favorites fetch timeout"));
166 }, 5000);
167 });
168 }
169
170 /**
171 * Toggle favorite status for a URL
172 */
173 toggleFavorite(url: string, title: string, faviconURL: string): void {
174 console.log("⭐ [DEBUG] Toggling favorite:", { url, title, faviconURL });
175 this.postMessage({ type: "toggle_favorite", url, title, faviconURL });
176 }
177
178 // Suggestions are returned via setSuggestions() when native handler responds
179}
180
181// Singleton instance
182export const omniboxBridge = new OmniboxBridge();
183
184/**
185 * Debounced query function for search input
186 */
187export function debouncedQuery(searchTerm: string): void {
188 omniboxStore.clearDebounceTimer();
189
190 const timerId = window.setTimeout(() => {
191 omniboxBridge.query(searchTerm);
192 }, omniboxStore.config.debounceDelay);
193
194 omniboxStore.setDebounceTimer(timerId);
195}
196
197// Extend global window interface for Go bridge compatibility
198declare global {
199 interface Window {
200 webkit?: {
201 messageHandlers?: {
202 dumber?: {
203 postMessage: (message: string) => void;
204 };
205 };
206 };
207 // Omnibox API for Go bridge
208 __dumber_omnibox?: {
209 setSuggestions: (suggestions: Suggestion[]) => void;
210 toggle: () => void;
211 open: (mode?: string, query?: string) => void;
212 close: () => void;
213 findQuery: (query: string) => void;
214 setActive: (active: boolean) => void;
215 };
216 }
217}