human (Nov 14, 2024, 07:32 PM)
I have this system that runs in a sandbox iframe and requests the parent page makes API requests on behalf of the child:
function makeRequest() {
const url = document.getElementById('urlInput').value;
const output = document.getElementById('output');
// Send message to parent window
window.parent.postMessage({
type: 'api-request',
url: url,
method: 'GET'
}, '*');
output.textContent = 'Loading...';
}
// Listen for response from parent
window.addEventListener('message', function(event) {
const output = document.getElementById('output');
if (event.data.type === 'api-response') {
if (event.data.error) {
output.textContent = 'Error: ' + event.data.error;
output.classList.add('error');
} else {
output.textContent = JSON.stringify(event.data.response, null, 2);
output.classList.remove('error');
}
}
});
Suggest ways I can make that API nicer to use. Ideally I'd like an async callAPi(url) function I can call which returns the results from a promise when they arrive from the parent
assistant (Nov 14, 2024, 07:32 PM)
I'll help you create a nicer Promise-based API wrapper for the postMessage communication. Here's a better way to structure this:
// Create a class to manage the API communication
class IframeAPIClient {
constructor(targetOrigin = '*') {
this.targetOrigin = targetOrigin;
this.pendingRequests = new Map();
this.requestId = 0;
// Set up message listener
window.addEventListener('message', this.handleMessage.bind(this));
}
handleMessage(event) {
if (event.data?.type !== 'api-response' || !event.data?.requestId) {
return;
}
const { requestId, error, response } = event.data;
const pendingRequest = this.pendingRequests.get(requestId);
if (!pendingRequest) {
return;
}
this.pendingRequests.delete(requestId);
if (error) {
pendingRequest.reject(new Error(error));
} else {
pendingRequest.resolve(response);
}
}
async callAPI(url, method = 'GET', body = null) {
const requestId = ++this.requestId;
const promise = new Promise((resolve, reject) => {
this.pendingRequests.set(requestId, { resolve, reject });
// Set a timeout to prevent hanging promises
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(new Error('Request timed out'));
}
}, 30000); // 30 second timeout
});
// Send the request to parent
window.parent.postMessage({
type: 'api-request',
requestId,
url,
method,
body
}, this.targetOrigin);
return promise;
}
// Convenience methods for different HTTP methods
get(url) {
return this.callAPI(url, 'GET');
}
post(url, body) {
return this.callAPI(url, 'POST', body);
}
put(url, body) {
return this.callAPI(url, 'PUT', body);
}
delete(url) {
return this.callAPI(url, 'DELETE');
}
}
// Create a singleton instance
const api = new IframeAPIClient();
// Example usage:
async function fetchData() {
try {
const data = await api.get('https://api.example.com/data');
console.log('Received data:', data);
} catch (error) {
console.error('API error:', error);
}
}
// Or use with Promise chaining
api.get('https://api.example.com/data')
.then(data => console.log('Received:', data))
.catch(error => console.error('Error:', error));
This implementation provides several improvements over the original:
-
Promise-based API: Each request returns a Promise that resolves with the response data or rejects with an error.
-
Request tracking: Uses unique request IDs to match responses with their corresponding requests, allowing multiple concurrent API calls.
-
Timeout handling: Includes a timeout mechanism to prevent hanging promises if the parent never responds.
-
Convenience methods: Provides helper methods for common HTTP verbs (get, post, put, delete).
-
Error handling: Proper error propagation through the Promise chain.
To use this in your iframe, you would just need to change your parent window handler to include the requestId in responses:
// In parent window
window.addEventListener('message', async function(event) {
if (event.data?.type === 'api-request') {
const { url, method, body, requestId } = event.data;
try {
const response = await fetch(url, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
event.source.postMessage({
type: 'api-response',
requestId,
response: data
}, '*');
} catch (error) {
event.source.postMessage({
type: 'api-response',
requestId,
error: error.message
}, '*');
}
}
});
Now you can make API calls from your iframe like this:
// Simple GET request
const data = await api.get('https://api.example.com/data');
// POST request with body
const response = await api.post('https://api.example.com/create', {
name: 'New Item',
value: 123
});
The code is much cleaner to use and handles error cases more robustly. You can also easily extend the IframeAPIClient
class to add more features like:
- Request cancellation
- Request queuing
- Retry logic
- Request/response interceptors
- Custom headers
- Response type handling (JSON, text, blob, etc.)
Would you like me to demonstrate how to implement any of these additional features?
human (Nov 14, 2024, 07:34 PM)
I like that. Ditch the post/put/delete methods, rename the class to just APIClient and rewrite this HTML here to use the new pattern instead
paste.txt
<!DOCTYPE html>
<html>
<head>
<title>API Explorer</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f0f0f0;
}
iframe {
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
transition: height 0.2s ease;
}
</style>
</head>
<body>
<iframe id="explorerFrame" sandbox="allow-scripts"></iframe>
<script>
// Create the HTML content for the iframe
const iframeContent = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
button {
padding: 8px 16px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0052a3;
}
pre {
background: #f5f5f5;
padding: 20px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.error {
color: #cc0000;
}
</style>
</head>
<body>
<div class="container">
<h1>API Explorer</h1>
<p>This is an experiment in <code><iframe></code> sandboxing and <code>window.parent.postMessage()</code>.</p>
<form onsubmit="makeRequest(); return false;">
<div class="input-group">
<input type="text" id="urlInput" placeholder="Enter API URL" value="https://datasette.io/content/stats.json?_size=1">
<button onclick="makeRequest()">Submit</button>
</div>
</form>
<pre id="output">// Response will appear here</pre>
</div>
<script>
let currentHeight = null;
function updateParentHeight() {
// Borrowed from https://github.com/FaiblUG/setIframeHeight/blob/5509feb0f281251d03065991b1c5f451f40fa8bd/dist/set-iframe-height-child.js#L31C1-L31C220
let newHeight = Math.min(
Math.max(
document.body.offsetHeight, document.documentElement.offsetHeight
),
Math.max(
Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight
),
Math.max(
document.body.clientHeight, document.documentElement.clientHeight
)
)
);
if (currentHeight === newHeight) {
return;
}
currentHeight = newHeight;
window.parent.postMessage({
type: 'resize',
height: newHeight
}, '*');
}
updateParentHeight();
setInterval(updateParentHeight, 1000);
function makeRequest() {
const url = document.getElementById('urlInput').value;
const output = document.getElementById('output');
// Send message to parent window
window.parent.postMessage({
type: 'api-request',
url: url,
method: 'GET'
}, '*');
output.textContent = 'Loading...';
}
// Listen for response from parent
window.addEventListener('message', function(event) {
const output = document.getElementById('output');
if (event.data.type === 'api-response') {
if (event.data.error) {
output.textContent = 'Error: ' + event.data.error;
output.classList.add('error');
} else {
output.textContent = JSON.stringify(event.data.response, null, 2);
output.classList.remove('error');
}
// Content has changed, update height
setTimeout(() => {
updateParentHeight();
}, 100);
}
});
SCUB
</body>
</html>
`.replace('SCUB', '</' + 'script>');
// Set up the iframe with the content
const iframe = document.getElementById('explorerFrame');
iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);
// Listen for messages from the iframe
window.addEventListener('message', async function(event) {
if (event.data.type === 'api-request') {
try {
const response = await fetch(event.data.url, {
method: event.data.method
});
const data = await response.json();
// Send response back to iframe
iframe.contentWindow.postMessage({
type: 'api-response',
response: data
}, '*');
} catch (error) {
// Send error back to iframe
iframe.contentWindow.postMessage({
type: 'api-response',
error: error.message
}, '*');
}
} else if (event.data.type === 'resize') {
// Update iframe height when child requests it
iframe.style.height = event.data.height + 'px';
}
});
</script>
</body>
</html>
assistant (Nov 14, 2024, 07:34 PM)
I'll simplify the APIClient class and integrate it into your HTML. Here's the rewritten version:
<!DOCTYPE html>
<html>
<head>
<title>API Explorer</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f0f0f0;
}
iframe {
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
transition: height 0.2s ease;
}
</style>
</head>
<body>
<iframe id="explorerFrame" sandbox="allow-scripts"></iframe>
<script>
// Create the HTML content for the iframe
const iframeContent = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
button {
padding: 8px 16px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0052a3;
}
pre {
background: #f5f5f5;
padding: 20px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.error {
color: #cc0000;
}
</style>
</head>
<body>
<div class="container">
<h1>API Explorer</h1>
<p>This is an experiment in <code><iframe></code> sandboxing and <code>window.parent.postMessage()</code>.</p>
<form onsubmit="makeRequest(); return false;">
<div class="input-group">
<input type="text" id="urlInput" placeholder="Enter API URL" value="https://datasette.io/content/stats.json?_size=1">
<button type="submit">Submit</button>
</div>
</form>
<pre id="output">// Response will appear here</pre>
</div>
<script>
class APIClient {
constructor(targetOrigin = '*') {
this.targetOrigin = targetOrigin;
this.pendingRequests = new Map();
this.requestId = 0;
// Set up message listener
window.addEventListener('message', this.handleMessage.bind(this));
}
handleMessage(event) {
if (event.data?.type !== 'api-response' || !event.data?.requestId) {
return;
}
const { requestId, error, response } = event.data;
const pendingRequest = this.pendingRequests.get(requestId);
if (!pendingRequest) {
return;
}
this.pendingRequests.delete(requestId);
if (error) {
pendingRequest.reject(new Error(error));
} else {
pendingRequest.resolve(response);
}
}
async callAPI(url) {
const requestId = ++this.requestId;
const promise = new Promise((resolve, reject) => {
this.pendingRequests.set(requestId, { resolve, reject });
// Set a timeout to prevent hanging promises
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(new Error('Request timed out'));
}
}, 30000); // 30 second timeout
});
// Send the request to parent
window.parent.postMessage({
type: 'api-request',
requestId,
url,
method: 'GET'
}, this.targetOrigin);
return promise;
}
}
const api = new APIClient();
// Height management
let currentHeight = null;
function updateParentHeight() {
let newHeight = Math.min(
Math.max(
document.body.offsetHeight, document.documentElement.offsetHeight
),
Math.max(
Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight
),
Math.max(
document.body.clientHeight, document.documentElement.clientHeight
)
)
);
if (currentHeight === newHeight) {
return;
}
currentHeight = newHeight;
window.parent.postMessage({
type: 'resize',
height: newHeight
}, '*');
}
updateParentHeight();
setInterval(updateParentHeight, 1000);
async function makeRequest() {
const url = document.getElementById('urlInput').value;
const output = document.getElementById('output');
output.textContent = 'Loading...';
try {
const response = await api.callAPI(url);
output.textContent = JSON.stringify(response, null, 2);
output.classList.remove('error');
} catch (error) {
output.textContent = 'Error: ' + error.message;
output.classList.add('error');
}
// Content has changed, update height
setTimeout(updateParentHeight, 100);
}
SCUB
</body>
</html>
`.replace('SCUB', '</' + 'script>');
// Set up the iframe with the content
const iframe = document.getElementById('explorerFrame');
iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframeContent);
// Listen for messages from the iframe
window.addEventListener('message', async function(event) {
if (event.data.type === 'api-request') {
try {
const response = await fetch(event.data.url, {
method: event.data.method
});
const data = await response.json();
// Send response back to iframe
iframe.contentWindow.postMessage({
type: 'api-response',
requestId: event.data.requestId,
response: data
}, '*');
} catch (error) {
// Send error back to iframe
iframe.contentWindow.postMessage({
type: 'api-response',
requestId: event.data.requestId,
error: error.message
}, '*');
}
} else if (event.data.type === 'resize') {
// Update iframe height when child requests it
iframe.style.height = event.data.height + 'px';
}
});
</script>
</body>
</html>
Key changes made:
-
Simplified the APIClient class by:
- Removing HTTP method-specific functions (post/put/delete)
- Renaming to just APIClient
- Simplifying callAPI to only handle GET requests
-
Updated the makeRequest function to use async/await with the new APIClient
- Now handles errors properly through try/catch
- Shows loading state
- Updates the output with either the response or error
-
Modified the parent window message handler to include requestId in responses
-
Added proper form submission handling with the type="submit" button
The code is now cleaner and easier to maintain while keeping all the original functionality. The Promise-based API makes error handling much more straightforward, and the loading states are handled properly.
Would you like to make any adjustments to this implementation?