Skip to content

Commit f9002ab

Browse files
authored
Merge pull request #133 from SentienceAPI/ordinal
Ordinal
2 parents a60a15b + e752502 commit f9002ab

File tree

11 files changed

+3794
-1255
lines changed

11 files changed

+3794
-1255
lines changed

sentience/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
ViewportRect,
7878
WaitResult,
7979
)
80+
81+
# Ordinal support (Phase 3)
82+
from .ordinal import OrdinalIntent, boost_ordinal_elements, detect_ordinal_intent, select_by_ordinal
8083
from .overlay import clear_overlay, show_overlay
8184
from .query import find, query
8285
from .read import read
@@ -242,4 +245,9 @@
242245
"all_of",
243246
"any_of",
244247
"custom",
248+
# Ordinal support (Phase 3)
249+
"OrdinalIntent",
250+
"detect_ordinal_intent",
251+
"select_by_ordinal",
252+
"boost_ordinal_elements",
245253
]

sentience/extension/background.js

Lines changed: 221 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,240 @@
11
import init, { analyze_page_with_options, analyze_page, prune_for_api } from "../pkg/sentience_core.js";
22

3-
let wasmReady = !1, wasmInitPromise = null;
3+
// background.js - Service Worker with WASM (CSP-Immune!)
4+
// This runs in an isolated environment, completely immune to page CSP policies
45

6+
7+
console.log('[Sentience Background] Initializing...');
8+
9+
// Global WASM initialization state
10+
let wasmReady = false;
11+
let wasmInitPromise = null;
12+
13+
/**
14+
* Initialize WASM module - called once on service worker startup
15+
* Uses static imports (not dynamic import()) which is required for Service Workers
16+
*/
517
async function initWASM() {
6-
if (!wasmReady) return wasmInitPromise || (wasmInitPromise = (async () => {
7-
try {
8-
globalThis.js_click_element = () => {}, await init(), wasmReady = !0;
9-
} catch (error) {
10-
throw error;
11-
}
12-
})(), wasmInitPromise);
13-
}
18+
if (wasmReady) return;
19+
if (wasmInitPromise) return wasmInitPromise;
1420

15-
async function handleScreenshotCapture(_tabId, options = {}) {
21+
wasmInitPromise = (async () => {
1622
try {
17-
const {format: format = "png", quality: quality = 90} = options;
18-
return await chrome.tabs.captureVisibleTab(null, {
19-
format: format,
20-
quality: quality
21-
});
23+
console.log('[Sentience Background] Loading WASM module...');
24+
25+
// Define the js_click_element function that WASM expects
26+
// In Service Workers, use 'globalThis' instead of 'window'
27+
// In background context, we can't actually click, so we log a warning
28+
globalThis.js_click_element = () => {
29+
console.warn('[Sentience Background] js_click_element called in background (ignored)');
30+
};
31+
32+
// Initialize WASM - this calls the init() function from the static import
33+
// The init() function handles fetching and instantiating the .wasm file
34+
await init();
35+
36+
wasmReady = true;
37+
console.log('[Sentience Background] ✓ WASM ready!');
38+
console.log(
39+
'[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api'
40+
);
2241
} catch (error) {
23-
throw new Error(`Failed to capture screenshot: ${error.message}`);
42+
console.error('[Sentience Background] WASM initialization failed:', error);
43+
throw error;
2444
}
45+
})();
46+
47+
return wasmInitPromise;
2548
}
2649

50+
// Initialize WASM on service worker startup
51+
initWASM().catch((err) => {
52+
console.error('[Sentience Background] Failed to initialize WASM:', err);
53+
});
54+
55+
/**
56+
* Message handler for all extension communication
57+
* Includes global error handling to prevent extension crashes
58+
*/
59+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
60+
// Global error handler to prevent extension crashes
61+
try {
62+
// Handle screenshot requests (existing functionality)
63+
if (request.action === 'captureScreenshot') {
64+
handleScreenshotCapture(sender.tab.id, request.options)
65+
.then((screenshot) => {
66+
sendResponse({ success: true, screenshot });
67+
})
68+
.catch((error) => {
69+
console.error('[Sentience Background] Screenshot capture failed:', error);
70+
sendResponse({
71+
success: false,
72+
error: error.message || 'Screenshot capture failed',
73+
});
74+
});
75+
return true; // Async response
76+
}
77+
78+
// Handle WASM processing requests (NEW!)
79+
if (request.action === 'processSnapshot') {
80+
handleSnapshotProcessing(request.rawData, request.options)
81+
.then((result) => {
82+
sendResponse({ success: true, result });
83+
})
84+
.catch((error) => {
85+
console.error('[Sentience Background] Snapshot processing failed:', error);
86+
sendResponse({
87+
success: false,
88+
error: error.message || 'Snapshot processing failed',
89+
});
90+
});
91+
return true; // Async response
92+
}
93+
94+
// Unknown action
95+
console.warn('[Sentience Background] Unknown action:', request.action);
96+
sendResponse({ success: false, error: 'Unknown action' });
97+
return false;
98+
} catch (error) {
99+
// Catch any synchronous errors that might crash the extension
100+
console.error('[Sentience Background] Fatal error in message handler:', error);
101+
try {
102+
sendResponse({
103+
success: false,
104+
error: `Fatal error: ${error.message || 'Unknown error'}`,
105+
});
106+
} catch (e) {
107+
// If sendResponse already called, ignore
108+
}
109+
return false;
110+
}
111+
});
112+
113+
/**
114+
* Handle screenshot capture (existing functionality)
115+
*/
116+
async function handleScreenshotCapture(_tabId, options = {}) {
117+
try {
118+
const { format = 'png', quality = 90 } = options;
119+
120+
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
121+
format,
122+
quality,
123+
});
124+
125+
console.log(
126+
`[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes`
127+
);
128+
return dataUrl;
129+
} catch (error) {
130+
console.error('[Sentience Background] Screenshot error:', error);
131+
throw new Error(`Failed to capture screenshot: ${error.message}`);
132+
}
133+
}
134+
135+
/**
136+
* Handle snapshot processing with WASM (NEW!)
137+
* This is where the magic happens - completely CSP-immune!
138+
* Includes safeguards to prevent crashes and hangs.
139+
*
140+
* @param {Array} rawData - Raw element data from injected_api.js
141+
* @param {Object} options - Snapshot options (limit, filter, etc.)
142+
* @returns {Promise<Object>} Processed snapshot result
143+
*/
27144
async function handleSnapshotProcessing(rawData, options = {}) {
28-
const startTime = performance.now();
145+
const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs
146+
const startTime = performance.now();
147+
148+
try {
149+
// Safety check: limit element count to prevent hangs
150+
if (!Array.isArray(rawData)) {
151+
throw new Error('rawData must be an array');
152+
}
153+
154+
if (rawData.length > MAX_ELEMENTS) {
155+
console.warn(
156+
`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`
157+
);
158+
rawData = rawData.slice(0, MAX_ELEMENTS);
159+
}
160+
161+
// Ensure WASM is initialized
162+
await initWASM();
163+
if (!wasmReady) {
164+
throw new Error('WASM module not initialized');
165+
}
166+
167+
console.log(
168+
`[Sentience Background] Processing ${rawData.length} elements with options:`,
169+
options
170+
);
171+
172+
// Run WASM processing using the imported functions directly
173+
// Wrap in try-catch with timeout protection
174+
let analyzedElements;
29175
try {
30-
if (!Array.isArray(rawData)) throw new Error("rawData must be an array");
31-
if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(),
32-
!wasmReady) throw new Error("WASM module not initialized");
33-
let analyzedElements, prunedRawData;
34-
try {
35-
const wasmPromise = new Promise((resolve, reject) => {
36-
try {
37-
let result;
38-
result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData),
39-
resolve(result);
40-
} catch (e) {
41-
reject(e);
42-
}
43-
});
44-
analyzedElements = await Promise.race([ wasmPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("WASM processing timeout (>18s)")), 18e3)) ]);
45-
} catch (e) {
46-
const errorMsg = e.message || "Unknown WASM error";
47-
throw new Error(`WASM analyze_page failed: ${errorMsg}`);
48-
}
176+
// Use a timeout wrapper to prevent infinite hangs
177+
const wasmPromise = new Promise((resolve, reject) => {
49178
try {
50-
prunedRawData = prune_for_api(rawData);
179+
let result;
180+
if (options.limit || options.filter) {
181+
result = analyze_page_with_options(rawData, options);
182+
} else {
183+
result = analyze_page(rawData);
184+
}
185+
resolve(result);
51186
} catch (e) {
52-
prunedRawData = rawData;
187+
reject(e);
53188
}
54-
performance.now();
55-
return {
56-
elements: analyzedElements,
57-
raw_elements: prunedRawData
58-
};
59-
} catch (error) {
60-
performance.now();
61-
throw error;
189+
});
190+
191+
// Add timeout protection (18 seconds - less than content.js timeout)
192+
analyzedElements = await Promise.race([
193+
wasmPromise,
194+
new Promise((_, reject) =>
195+
setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000)
196+
),
197+
]);
198+
} catch (e) {
199+
const errorMsg = e.message || 'Unknown WASM error';
200+
console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e);
201+
throw new Error(`WASM analyze_page failed: ${errorMsg}`);
62202
}
63-
}
64203

65-
initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
204+
// Prune elements for API (prevents 413 errors on large sites)
205+
let prunedRawData;
66206
try {
67-
return "captureScreenshot" === request.action ? (handleScreenshotCapture(sender.tab.id, request.options).then(screenshot => {
68-
sendResponse({
69-
success: !0,
70-
screenshot: screenshot
71-
});
72-
}).catch(error => {
73-
sendResponse({
74-
success: !1,
75-
error: error.message || "Screenshot capture failed"
76-
});
77-
}), !0) : "processSnapshot" === request.action ? (handleSnapshotProcessing(request.rawData, request.options).then(result => {
78-
sendResponse({
79-
success: !0,
80-
result: result
81-
});
82-
}).catch(error => {
83-
sendResponse({
84-
success: !1,
85-
error: error.message || "Snapshot processing failed"
86-
});
87-
}), !0) : (sendResponse({
88-
success: !1,
89-
error: "Unknown action"
90-
}), !1);
91-
} catch (error) {
92-
try {
93-
sendResponse({
94-
success: !1,
95-
error: `Fatal error: ${error.message || "Unknown error"}`
96-
});
97-
} catch (e) {}
98-
return !1;
207+
prunedRawData = prune_for_api(rawData);
208+
} catch (e) {
209+
console.warn('[Sentience Background] prune_for_api failed, using original data:', e);
210+
prunedRawData = rawData;
99211
}
100-
}), self.addEventListener("error", event => {
101-
event.preventDefault();
102-
}), self.addEventListener("unhandledrejection", event => {
103-
event.preventDefault();
104-
});
212+
213+
const duration = performance.now() - startTime;
214+
console.log(
215+
`[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)`
216+
);
217+
218+
return {
219+
elements: analyzedElements,
220+
raw_elements: prunedRawData,
221+
};
222+
} catch (error) {
223+
const duration = performance.now() - startTime;
224+
console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error);
225+
throw error;
226+
}
227+
}
228+
229+
console.log('[Sentience Background] Service worker ready');
230+
231+
// Global error handlers to prevent extension crashes
232+
self.addEventListener('error', (event) => {
233+
console.error('[Sentience Background] Global error caught:', event.error);
234+
event.preventDefault(); // Prevent extension crash
235+
});
236+
237+
self.addEventListener('unhandledrejection', (event) => {
238+
console.error('[Sentience Background] Unhandled promise rejection:', event.reason);
239+
event.preventDefault(); // Prevent extension crash
240+
});

0 commit comments

Comments
 (0)