Skip to content

Add a ready promise #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,15 @@ <h1>%VITE_TITLE% Demo</h1>
</div>
</div>

<iframe
id="jupyterlab"
src="%VITE_DEMO_SRC%"
sandbox="allow-scripts allow-same-origin"
title="JupyterLab Instance"
loading="lazy"
></iframe>
<div class="iframe-container">
<div id="bridge-status" class="status-indicator">Connecting to JupyterLab...</div>
<iframe
id="jupyterlab"
src="%VITE_DEMO_SRC%"
sandbox="allow-scripts allow-same-origin"
title="JupyterLab Instance"
loading="lazy"
></iframe>
</div>
</body>
</html>
20 changes: 20 additions & 0 deletions demo/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ import { createBridge } from 'jupyter-iframe-commands-host';

const commandBridge = createBridge({ iframeId: 'jupyterlab' });

const statusIndicator = document.getElementById('bridge-status');
statusIndicator.style.backgroundColor = '#ffa500'; // Orange for connecting

let bridgeReady = false;

const submitCommand = async (command, args) => {
// Don't allow command execution until bridge is ready
if (!bridgeReady) {
document.getElementById('error-dialog').innerHTML =
'<code>Command bridge is not ready yet. Please wait.</code>';
errorDialog.showModal();
return;
}

try {
await commandBridge.execute(command, args ? JSON.parse(args) : {});
} catch (e) {
Expand Down Expand Up @@ -157,3 +170,10 @@ modeRadios.forEach(radio => {
iframe.src = currentUrl.toString();
});
});

// Wait for the command bridge to be ready
commandBridge.ready.then(() => {
bridgeReady = true;
statusIndicator.textContent = 'Connected to JupyterLab';
statusIndicator.style.backgroundColor = '#32CD32'; // Green for connected
});
19 changes: 19 additions & 0 deletions demo/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,22 @@ dialog button[value='close']:hover {
font-weight: 500;
user-select: none;
}

.status-indicator {
padding: 5px 10px;
border-radius: 4px;
color: #fff;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
display: inline-block;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.iframe-container {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
8 changes: 5 additions & 3 deletions packages/extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
},
async listCommands() {
return commands.listCommands();
},
get ready() {
return app.started;
}
};

const endpoint = windowEndpoint(self.parent);
expose(api, endpoint);

app.started.then(() => {
const endpoint = windowEndpoint(self.parent);
expose(api, endpoint);
window.parent?.postMessage('extension-loaded', '*');
});
}
Expand Down
5 changes: 5 additions & 0 deletions packages/extension/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ export interface ICommandBridgeRemote {
* @returns An array of strings representing the names of all available commands.
*/
listCommands(): Promise<string[]>;

/**
* Waits for the JupyterLab environment to be ready.
*/
ready: Promise<void>;
}
48 changes: 46 additions & 2 deletions packages/host/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import { windowEndpoint, wrap, proxy, ProxyOrClone } from 'comlink';

import { ICommandBridgeRemote } from 'jupyter-iframe-commands';

/**
* A bridge to expose actions on JupyterLab commands.
*/
export function createBridge({ iframeId }: { iframeId: string }) {
export function createBridge({
iframeId
}: {
iframeId: string;
}): ICommandBridgeRemote {
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;

if (!iframe) {
Expand All @@ -21,7 +26,46 @@ export function createBridge({ iframeId }: { iframeId: string }) {
);
}

return wrap<ICommandBridgeRemote>(windowEndpoint(iframe.contentWindow));
const wrappedBridge = wrap<ICommandBridgeRemote>(
windowEndpoint(iframe.contentWindow)
);

// Create a promise that resolves when the iframe signals it's ready
const readyPromise = new Promise<void>(resolve => {
const controller = new AbortController();
const signal = controller.signal;

const messageHandler = (event: MessageEvent) => {
if (event.data === 'extension-loaded') {
controller.abort();
resolve();
}
};

window.addEventListener('message', messageHandler, { signal });

wrappedBridge.ready
.then(() => {
controller.abort();
resolve();
})
.catch(() => {
// If ready() fails, we'll still wait for the message
// No need to do anything, the message listener is still active
});
});

// Create a proxy that intercepts property access
return new Proxy(wrappedBridge, {
get(target, prop, receiver) {
// If the property is 'ready', return the ready promise
if (prop === 'ready') {
return readyPromise;
}
// Otherwise delegate to the comlink wrapped bridge
return Reflect.get(target, prop, receiver);
}
});
}

/**
Expand Down
Loading