Skip to content

Commit 4e6e599

Browse files
fix: resolve commlink behavior between server and worker
1 parent 66fe76a commit 4e6e599

File tree

3 files changed

+284
-10
lines changed

3 files changed

+284
-10
lines changed

frontend/src/utils/websocket.js

Lines changed: 256 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2824,29 +2824,77 @@ class TransportSelector {
28242824
environment.supportsModules = 'noModule' in HTMLScriptElement.prototype;
28252825
environment.isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
28262826

2827+
// Enhanced HTML export detection
2828+
environment.isHtmlExport = this._detectHtmlExport();
2829+
environment.hasProjectFs = typeof fetch !== 'undefined' && this._checkForProjectFs();
2830+
28272831
return environment;
28282832
}
28292833

2834+
static _detectHtmlExport() {
2835+
try {
2836+
// Check for HTML export indicators
2837+
return (
2838+
// Check for explicit client type setting
2839+
window.__PRESWALD_CLIENT_TYPE === 'comlink' ||
2840+
// Check for project_fs.json presence (HTML export artifact)
2841+
document.querySelector('script').textContent?.includes('project_fs.json') ||
2842+
// Check for Pyodide-related scripts
2843+
document.querySelector('script[src*="pyodide"]') ||
2844+
// Check for inline script setting client type
2845+
Array.from(document.querySelectorAll('script')).some(script =>
2846+
script.textContent?.includes('window.__PRESWALD_CLIENT_TYPE')
2847+
) ||
2848+
// Check for static file serving pattern (no dynamic server endpoints)
2849+
window.location.protocol === 'file:' ||
2850+
// Check if we're being served from a static file server with typical HTML export structure
2851+
(window.location.pathname.endsWith('.html') || window.location.pathname.endsWith('/'))
2852+
);
2853+
} catch (error) {
2854+
console.warn('[TransportSelector] Error detecting HTML export environment:', error);
2855+
return false;
2856+
}
2857+
}
2858+
2859+
static _checkForProjectFs() {
2860+
// Quick check to see if project_fs.json is available (HTML export indicator)
2861+
try {
2862+
// Don't actually fetch, just check if we can construct the URL
2863+
const projectFsUrl = new URL('project_fs.json', window.location.origin + window.location.pathname);
2864+
return projectFsUrl.href !== null;
2865+
} catch {
2866+
return false;
2867+
}
2868+
}
2869+
28302870
static selectForEnvironment(environment, config) {
2831-
// Priority-based selection with fallbacks
2871+
// Priority 1: HTML export environment should always use Comlink
2872+
if (environment.isHtmlExport && environment.hasWorkers && config.enableWorkers !== false) {
2873+
console.log('[TransportSelector] HTML export environment detected, using Comlink transport');
2874+
return TransportType.COMLINK;
2875+
}
2876+
2877+
// Priority 2: Embedded contexts (iframes) should use PostMessage
28322878
if (environment.isEmbedded && environment.hasPostMessage) {
28332879
return TransportType.POST_MESSAGE;
28342880
}
28352881

2836-
if (environment.hasWebSocket && !environment.isEmbedded) {
2882+
// Priority 3: Regular web applications with server connectivity use WebSocket
2883+
if (environment.hasWebSocket && !environment.isEmbedded && !environment.isHtmlExport) {
28372884
return TransportType.WEBSOCKET;
28382885
}
28392886

2887+
// Priority 4: Worker-based applications (development/testing)
28402888
if (environment.hasWorkers && config.enableWorkers !== false) {
28412889
return TransportType.COMLINK;
28422890
}
28432891

2844-
// Fallback to PostMessage if available
2892+
// Priority 5: Fallback to PostMessage if available
28452893
if (environment.hasPostMessage) {
28462894
return TransportType.POST_MESSAGE;
28472895
}
28482896

2849-
// Last resort fallback
2897+
// Last resort fallback (should rarely be reached)
28502898
console.warn('[TransportSelector] No optimal transport found, defaulting to WebSocket');
28512899
return TransportType.WEBSOCKET;
28522900
}
@@ -3271,8 +3319,211 @@ const serverConfigPromise = initializeServerConfiguration();
32713319
/**
32723320
* Create the default communication layer with intelligent server resolution
32733321
* This is the main export that most applications will use
3322+
*
3323+
* For HTML exports, we defer creation until the DOM is ready to ensure
3324+
* window.__PRESWALD_CLIENT_TYPE is set correctly.
32743325
*/
3275-
export const comm = createCommunicationLayer();
3326+
let _globalCommInstance = null;
3327+
3328+
function getOrCreateCommunicationLayer() {
3329+
if (_globalCommInstance) {
3330+
return _globalCommInstance;
3331+
}
3332+
3333+
try {
3334+
// Enhanced HTML export detection using multiple strategies
3335+
const isHtmlExport = detectHtmlExportEnvironment();
3336+
const clientType = window.__PRESWALD_CLIENT_TYPE;
3337+
3338+
console.log(`[WebSocket Module] Environment analysis:`, {
3339+
isHtmlExport,
3340+
clientType,
3341+
pathname: window.location.pathname,
3342+
protocol: window.location.protocol,
3343+
port: window.location.port,
3344+
hasProjectFsInUrl: window.location.href.includes('project_fs.json')
3345+
});
3346+
3347+
// Priority 1: Explicit client type 'comlink' always uses Comlink
3348+
if (clientType === 'comlink' || clientType === TransportType.COMLINK) {
3349+
console.log(`[WebSocket Module] Using Comlink transport due to explicit client type: ${clientType}`);
3350+
_globalCommInstance = createCommunicationLayer({
3351+
transport: TransportType.COMLINK,
3352+
enableUrlParams: false,
3353+
enableStorage: false
3354+
});
3355+
}
3356+
// Priority 2: Check if we have a server available (even in HTML export environment)
3357+
else if (isHtmlExport) {
3358+
console.log(`[WebSocket Module] HTML export environment detected, checking server availability...`);
3359+
3360+
// Check if we can reach a server (quick check)
3361+
const hasServerConnection = checkServerAvailability();
3362+
3363+
if (hasServerConnection) {
3364+
console.log(`[WebSocket Module] Server available in HTML export environment, using WebSocket transport`);
3365+
_globalCommInstance = createCommunicationLayer({
3366+
transport: TransportType.WEBSOCKET
3367+
});
3368+
} else {
3369+
console.log(`[WebSocket Module] No server available in HTML export environment, using Comlink transport`);
3370+
_globalCommInstance = createCommunicationLayer({
3371+
transport: TransportType.COMLINK,
3372+
enableUrlParams: false,
3373+
enableStorage: false
3374+
});
3375+
}
3376+
}
3377+
// Priority 3: Other explicit client types
3378+
else if (clientType && clientType !== TransportType.AUTO) {
3379+
console.log(`[WebSocket Module] Explicit client type specified: ${clientType}`);
3380+
_globalCommInstance = createCommunicationLayer({ transport: clientType });
3381+
}
3382+
// Priority 4: Default auto-detection
3383+
else {
3384+
console.log(`[WebSocket Module] Using auto-detection for transport selection`);
3385+
_globalCommInstance = createCommunicationLayer();
3386+
}
3387+
3388+
return _globalCommInstance;
3389+
} catch (error) {
3390+
console.error('[WebSocket Module] Error creating communication layer:', error);
3391+
// Fallback to basic creation
3392+
_globalCommInstance = createCommunicationLayer();
3393+
return _globalCommInstance;
3394+
}
3395+
}
3396+
3397+
function checkServerAvailability() {
3398+
try {
3399+
// Quick synchronous check to see if we're in a server environment
3400+
// Look for indicators that suggest a server is available
3401+
3402+
// 1. Check if we're on a known server port that's responding
3403+
const currentPort = window.location.port;
3404+
const isServerPort = ['8501', '8000', '3000', '5000'].includes(currentPort);
3405+
3406+
// 2. Check if localStorage has a server URL (indicates previous server connection)
3407+
const storedServerUrl = localStorage.getItem('preswald_server_url');
3408+
3409+
// 3. Check if we can access server-specific endpoints (quick check)
3410+
const hasServerEndpoints = window.location.pathname.includes('/api/') ||
3411+
window.location.pathname.includes('/ws/') ||
3412+
window.location.search.includes('server=');
3413+
3414+
// 4. Check if project_fs.json is NOT available locally (indicates server environment)
3415+
const hasLocalProjectFs = checkForProjectFsSync();
3416+
3417+
console.log(`[WebSocket Module] Server availability check:`, {
3418+
isServerPort,
3419+
storedServerUrl: !!storedServerUrl,
3420+
hasServerEndpoints,
3421+
hasLocalProjectFs
3422+
});
3423+
3424+
// If we have server indicators and no local project_fs.json, likely server environment
3425+
const hasServerConnection = (isServerPort || storedServerUrl || hasServerEndpoints) && !hasLocalProjectFs;
3426+
3427+
return hasServerConnection;
3428+
} catch (error) {
3429+
console.warn('[WebSocket Module] Error checking server availability:', error);
3430+
return false;
3431+
}
3432+
}
3433+
3434+
function detectHtmlExportEnvironment() {
3435+
try {
3436+
// Primary detection: Check for explicit client type first
3437+
if (window.__PRESWALD_CLIENT_TYPE === 'comlink') {
3438+
console.log('[WebSocket Module] HTML export detected via explicit client type: comlink');
3439+
return true;
3440+
}
3441+
3442+
// Secondary detection: Check for project_fs.json existence (synchronous check)
3443+
const hasProjectFs = checkForProjectFsSync();
3444+
if (hasProjectFs) {
3445+
console.log('[WebSocket Module] HTML export detected via project_fs.json presence');
3446+
return true;
3447+
}
3448+
3449+
// Tertiary detection: Multiple indicators approach
3450+
const indicators = [
3451+
// 1. Check for typical HTML export URL patterns
3452+
window.location.pathname.endsWith('.html') ||
3453+
window.location.pathname.endsWith('/') ||
3454+
window.location.protocol === 'file:',
3455+
3456+
// 2. Check for HTML export script indicators in head
3457+
Array.from(document.head?.querySelectorAll('script') || []).some(script =>
3458+
script.textContent?.includes('window.__PRESWALD_CLIENT_TYPE') ||
3459+
script.textContent?.includes('project_fs.json')
3460+
),
3461+
3462+
// 3. Check for absence of typical server endpoints
3463+
!window.location.pathname.includes('/api/') &&
3464+
!window.location.pathname.includes('/ws/') &&
3465+
!window.location.search.includes('server='),
3466+
3467+
// 4. Check if running from static file server (common HTML export pattern)
3468+
window.location.port && ['8080', '8081', '3000', '5000'].includes(window.location.port) &&
3469+
!window.location.pathname.includes('dev'),
3470+
3471+
// 5. Check for Pyodide or worker-related assets
3472+
Array.from(document.querySelectorAll('script[src]')).some(script =>
3473+
script.src.includes('pyodide') || script.src.includes('worker')
3474+
)
3475+
];
3476+
3477+
// Consider it an HTML export if multiple indicators are present
3478+
const positiveIndicators = indicators.filter(Boolean).length;
3479+
const isHtmlExport = positiveIndicators >= 2;
3480+
3481+
if (isHtmlExport) {
3482+
console.log(`[WebSocket Module] HTML export detected with ${positiveIndicators} indicators:`, {
3483+
clientType: window.__PRESWALD_CLIENT_TYPE,
3484+
pathname: window.location.pathname,
3485+
protocol: window.location.protocol,
3486+
port: window.location.port,
3487+
hasProjectFsScript: indicators[1],
3488+
noServerEndpoints: indicators[2],
3489+
staticFileServer: indicators[3],
3490+
hasWorkerAssets: indicators[4]
3491+
});
3492+
}
3493+
3494+
return isHtmlExport;
3495+
} catch (error) {
3496+
console.warn('[WebSocket Module] Error detecting HTML export environment:', error);
3497+
return false;
3498+
}
3499+
}
3500+
3501+
function checkForProjectFsSync() {
3502+
try {
3503+
// Check if project_fs.json is accessible (this is the most reliable indicator)
3504+
const xhr = new XMLHttpRequest();
3505+
xhr.open('HEAD', './project_fs.json', false); // Synchronous request
3506+
xhr.send();
3507+
return xhr.status === 200;
3508+
} catch (error) {
3509+
// If we get a CORS error or network error, we might still be in HTML export
3510+
// but served from a different origin. Check for other indicators.
3511+
return false;
3512+
}
3513+
}
3514+
3515+
export const comm = new Proxy({}, {
3516+
get(target, prop) {
3517+
const instance = getOrCreateCommunicationLayer();
3518+
const value = instance[prop];
3519+
return typeof value === 'function' ? value.bind(instance) : value;
3520+
},
3521+
set(target, prop, value) {
3522+
const instance = getOrCreateCommunicationLayer();
3523+
instance[prop] = value;
3524+
return true;
3525+
}
3526+
});
32763527

32773528
/**
32783529
* Create a communication layer with explicit server URL configuration

preswald/browser/boot.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
*/
55
async function boot() {
66
console.log('[Boot] Starting boot process...');
7-
// await new Promise(resolve => setTimeout(resolve, 20000));
8-
const comm = window.__PRESWALD_COMM;
7+
8+
// Wait for the communication layer to be initialized
9+
let comm = window.__PRESWALD_COMM;
10+
let retryCount = 0;
11+
const maxRetries = 50; // 5 seconds with 100ms intervals
12+
13+
while (!comm && retryCount < maxRetries) {
14+
console.log(`[Boot] Waiting for PRESWALD_COMM... (${retryCount + 1}/${maxRetries})`);
15+
await new Promise(resolve => setTimeout(resolve, 100));
16+
comm = window.__PRESWALD_COMM;
17+
retryCount++;
18+
}
919

1020
if (!comm) {
11-
console.error('[Boot] Error: window.__PRESWALD_COMM is not initialized');
21+
console.error('[Boot] Error: window.__PRESWALD_COMM is not initialized after timeout');
22+
console.error('[Boot] Make sure the communication layer is properly set up for HTML export');
1223
return;
1324
}
1425

preswald/utils.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,9 @@ def prepare_html_export(
523523
logger.info(f"Copied Preswald static assets to {output_dir}")
524524

525525
# 3. Modify index.html (add branding, boot script)
526-
head_script, _ = get_boot_script_html(
526+
head_script, body_script = get_boot_script_html(
527527
client_type=client_type
528-
) # body_script not used by current logic
528+
)
529529

530530
# Initialize branding manager
531531
# For BrandingManager, static_dir is the path to package's static files (e.g., .../preswald/static)
@@ -584,6 +584,18 @@ def prepare_html_export(
584584
else: # Fallback if </head> not found
585585
index_content = index_content + "\n" + scripts_to_inject
586586

587+
# Add the boot script to the body (before </body>)
588+
if "</body>" in index_content:
589+
# Extract just the script tag from body_script (remove </body></html>)
590+
boot_script_tag = body_script.split("</body>")[0].strip()
591+
index_content = index_content.replace(
592+
"</body>", f"{boot_script_tag}\n</body>"
593+
)
594+
else: # Fallback if </body> not found
595+
# Extract just the script tag from body_script
596+
boot_script_tag = body_script.split("</body>")[0].strip()
597+
index_content = index_content + "\n" + boot_script_tag
598+
587599
f.seek(0)
588600
f.write(index_content)
589601
f.truncate()

0 commit comments

Comments
 (0)