UNPKG

@codesandbox/sandpack-client

Version:

<img style="width:100%" src="https://user-images.githubusercontent.com/4838076/143581035-ebee5ba2-9cb1-4fe8-a05b-2f44bd69bb4b.gif" alt="Component toolkit for live running code editing experiences" />

275 lines (224 loc) 6.85 kB
/* eslint-disable */ /** * Utils */ const CHANNEL_NAME = '$CSB_RELAY'; function invariant(predicate, message, ...positionals) { if (!predicate) { throw new Error(message, ...positionals); } } let counter = 0; function generateRandomId() { const now = Date.now(); const randomNumber = Math.round(Math.random() * 10000); counter += 1; return (+`${now}${randomNumber}${counter}`).toString(16); } const DEBUG = false; const debug = (...args) => { if (DEBUG) console.debug(...args); }; function DeferredPromise() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); promise.state = 'pending'; promise.resolve = data => { if (promise.state !== 'pending') { return; } promise.result = data; const onFulfilled = value => { promise.state = 'fulfilled'; return value; }; return resolve( data instanceof Promise ? data : Promise.resolve(data).then(onFulfilled) ); }; promise.reject = reason => { if (promise.state !== 'pending') { return; } queueMicrotask(() => { promise.state = 'rejected'; }); return reject((promise.rejectionReason = reason)); }; return promise; } /** * End utils */ self.addEventListener('install', function () { self.skipWaiting(); }); self.addEventListener('activate', () => { return self.clients.claim(); }); const pendingRequests = new Map(); function initRelayPort(relayPort) { /** * @note that "addEventListener" and "onmessage" are not always * synonymous in MessageChannel so be careful refactoring this. */ relayPort.onmessage = event => { const { data } = event; switch (data.$type) { case 'preview/response': { const message = data; const foundRequest = pendingRequests.get(message.id); // No pending request associated with the request ID from the message is a no-op. invariant( foundRequest, 'Failed to handle "PREVIEW_RESPONSE_TYPE" message from the relay: unknown request ID "%s"', message.id ); pendingRequests.delete(message.id); foundRequest.resolve({ status: message.status, headers: message.headers, body: message.body, }); break; } } }; } function createRelayPortPromise() { const promise = new DeferredPromise(); promise.then(port => { initRelayPort(port); return port; }); return promise; } // Declare a promise that resolves once the relay sends // a message with the MessagePort to bind their communication. let relayPortPromise = createRelayPortPromise(); async function sendToRelay(message) { const relayPort = await relayPortPromise; invariant( relayPort, 'Failed to send message to the relay: relay message port is not defined', message ); relayPort.postMessage(message); } self.addEventListener('message', async event => { if (typeof event.data !== 'object' || event.data.$channel !== CHANNEL_NAME) { return; } const message = event.data; switch (message.$type) { case 'worker/invalidate-port': { debug('[SW]: Invalidate port'); relayPortPromise = createRelayPortPromise(); break; } case 'worker/init': { const nextRelayPort = event.ports[0]; invariant( relayPortPromise.state === 'pending', 'Failed to initialize relay: relay port promise already fulfilled from previous evaluation.' ); /** * @fixme Looks like upon shell restart, the worker is still running, * so this promise is already resolved. Resolving it again with * the correct message port does nothing, and it keeps pointing * to the previous (incorrect) message port. */ relayPortPromise.resolve(nextRelayPort); debug('[SW]: relay port resolved', nextRelayPort); break; } case 'worker/ping': { // We are only interested in clients sending this. if (!(event.source instanceof Client)) { return; } const client = await self.clients.get(event.source.id); if (client) { const pong = { $channel: CHANNEL_NAME, $type: 'worker/pong', }; // Send back the pong message to keep the client/worker // communication from becoming idle (i.e. terminated). client.postMessage(pong); } break; } } }); function getResponse(request) { const requestId = generateRandomId(); const requestPromise = new DeferredPromise(); // Add some response timeout so the worker doesn't hang indefinitely, // making it hard to know what went wrong. const timeout = setTimeout(() => { pendingRequests.delete(requestId); requestPromise.reject( new Error( `Failed to handle ${request.method} ${request.url} request: no response received from the BroadcastChannel within timeout. There's likely an issue with the relay/worker communication.` ) ); }, 10000); const requestMessage = { $channel: CHANNEL_NAME, $type: 'preview/request', id: requestId, url: request.url, method: request.method, }; pendingRequests.set(requestId, requestPromise); // The worker delegates request resolution to the relay, which, // in turn, forwards these request messages to the main frame. sendToRelay(requestMessage); return requestPromise.finally(() => clearTimeout(timeout)); } self.addEventListener('fetch', event => { const req = event.request.clone(); const parsedUrl = new URL(req.url); /** * TODO: figure out a way to filter bundler requests and * local requests. Now, it only supports `public` */ if ( parsedUrl.origin === self.location.origin && (parsedUrl.pathname.startsWith('/public') || parsedUrl.pathname.startsWith('/node_modules/')) ) { const handleRequest = async () => { debug(`[SW]: request `, req); const response = await getResponse(req); const pathname = parsedUrl.pathname; const responseBody = pathname.endsWith('.woff2') || pathname.endsWith('.woff') || pathname.endsWith('.ttf') ? base64ToUint8Array(response.body) : response.body; const swResponse = new Response(responseBody, { headers: response.headers, status: response.status, }); debug(`[SW]: response`, response); return swResponse; }; // eslint-disable-next-line return event.respondWith(handleRequest()); } }); function base64ToUint8Array(base64String) { const binaryString = atob(base64String); const uint8Array = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { uint8Array[i] = binaryString.charCodeAt(i); } return uint8Array; }