Documentation Index Fetch the complete documentation index at: https://mintlify.com/rbbydotdev/opal/llms.txt
Use this file to discover all available pages before exploring further.
Opal Editor leverages service workers to enable powerful offline-first capabilities, including content rendering, image optimization, and local file serving without any backend server.
Architecture Overview
The service worker acts as a programmable network proxy that intercepts all HTTP requests from the editor and handles them locally.
// Service worker registration
await navigator . serviceWorker . register ( '/sw.js' , {
scope: '/' ,
updateViaCache: 'none' ,
});
How It Works
Registration : Service worker registers on first load
Interception : All fetch requests are intercepted
Local Processing : Requests are handled using local storage
Response : Results returned without touching the network
Request Routing
Opal uses Hono, a lightweight web framework, to route requests within the service worker:
// From sw-hono.ts
const app = new Hono ();
// Route definitions
app . get ( '/api/workspace/:workspaceName/file/*' , handleFileRequest );
app . get ( '/api/workspace/:workspaceName/image/*' , handleImageRequest );
app . post ( '/api/workspace/:workspaceName/search' , handleWorkspaceSearch );
app . get ( '/download.zip' , handleDownloadRequest );
Request Types
The service worker handles several categories of requests:
File Serving Serves workspace files directly from IndexedDB/OPFS
Image Processing Automatic WebP conversion and caching
Markdown Rendering Server-side markdown to HTML conversion
Search Operations Full-text and filename search across workspace
Key Features
1. Offline File Serving
Serve files from local storage as if they came from a server:
// Request a workspace file
fetch ( '/api/workspace/my-blog/file/posts/hello.md' )
. then ( res => res . text ())
. then ( content => {
// File served from local storage
console . log ( content );
});
The service worker:
Resolves the workspace name from the URL
Loads the workspace from local storage
Reads the file from the disk
Returns it as an HTTP response
2. Markdown Rendering
Render markdown to HTML without a backend:
// From handleMarkdownRender.ts
app . get ( '/api/render/markdown' , async ( c ) => {
const { workspaceName , documentId , editId } = c . req . query ();
// Load workspace from local storage
const workspace = await SWWStore . getWorkspace ( workspaceName );
// Get document content
const content = await workspace . readFile ( documentId );
// Render to HTML
const html = await renderMarkdown ( content );
return c . html ( html );
});
Features:
Syntax highlighting for code blocks
GitHub-flavored markdown
Table of contents generation
Image URL resolution
Link transformations
3. Image Optimization
Automatic image format conversion and caching:
// From handleImageRequest.ts
app . get ( '/api/workspace/:workspaceName/image/*' , async ( c ) => {
const imagePath = c . req . param ( '*' );
const workspace = await resolveWorkspace ( c );
// Read original image
const imageData = await workspace . readFile ( imagePath );
// Convert to WebP for better compression
const webpData = await convertToWebP ( imageData );
// Cache the converted image
await cache . put ( imagePath , webpData );
return c . body ( webpData , 200 , {
'Content-Type' : 'image/webp' ,
'Cache-Control' : 'max-age=31536000'
});
});
4. Workspace Search
Full-text search across all workspace files:
// From handleWorkspaceSearch.ts
app . post ( '/api/workspace/:workspaceName/search' , async ( c ) => {
const { searchTerm , regexp } = await c . req . json ();
const workspace = await resolveWorkspace ( c );
// Search all text files
const results = [];
for await ( const { filePath , text } of workspace . disk . scan ()) {
if ( regexp ) {
const regex = new RegExp ( searchTerm , 'gi' );
const matches = [ ... text . matchAll ( regex )];
if ( matches . length ) {
results . push ({ filePath , matches });
}
} else {
if ( text . includes ( searchTerm )) {
results . push ({ filePath });
}
}
}
return c . json ( results );
});
Search capabilities:
Full-text content search
Filename search with fuzzy matching
Regular expression support
Streaming results for large workspaces
Case-insensitive matching
5. ZIP Export
Export entire workspace as a ZIP file:
// From handleDownloadRequest.ts
app . get ( '/download.zip' , async ( c ) => {
const { workspaceName , password , encryption } = c . req . query ();
const workspace = await resolveWorkspace ( c );
// Create ZIP stream
const zipStream = new ReadableStream ({
async start ( controller ) {
for await ( const node of workspace . disk . fileTree . iterator ()) {
if ( node . isFile ()) {
const content = await workspace . readFile ( node . path );
controller . enqueue ({
path: node . path ,
content: content
});
}
}
controller . close ();
}
});
// Optionally encrypt
if ( password ) {
return encryptedZipStream ( zipStream , password , encryption );
}
return c . body ( zipStream , 200 , {
'Content-Type' : 'application/zip' ,
'Content-Disposition' : `attachment; filename=" ${ workspaceName } .zip"`
});
});
Request Signaling
Service workers can’t directly communicate with the main thread, so Opal uses a request signaling system:
// From RequestSignals.tsx
class RequestSignals {
// Signal a request to the service worker
signal ( requestId : string , data : any ) {
const event = new CustomEvent ( 'REQ_SIGNAL' , {
detail: { requestId , data }
});
self . dispatchEvent ( event );
}
// Wait for response
async waitForResponse ( requestId : string ) : Promise < any > {
return new Promise (( resolve ) => {
const handler = ( event : CustomEvent ) => {
if ( event . detail . requestId === requestId ) {
resolve ( event . detail . data );
self . removeEventListener ( 'REQ_RESPONSE' , handler );
}
};
self . addEventListener ( 'REQ_RESPONSE' , handler );
});
}
}
Use cases:
Long-running operations (search, export)
Progress reporting
Cancellation support
Bidirectional communication
Service Worker Lifecycle
Installation
self . addEventListener ( 'install' , ( event ) => {
console . log ( 'Service worker installing...' );
// Skip waiting to activate immediately
event . waitUntil ( self . skipWaiting ());
});
Activation
self . addEventListener ( 'activate' , ( event ) => {
console . log ( 'Service worker activated' );
// Take control of all pages immediately
event . waitUntil ( self . clients . claim ());
});
Fetch Handling
self . addEventListener ( 'fetch' , ( event ) => {
// Use Hono to handle the request
event . respondWith ( handle ( event . request ));
});
Caching Strategy
Opal uses multiple cache levels:
// Cache layers
1. Memory cache ( fastest , volatile )
2. Cache API ( fast , persistent )
3. IndexedDB / OPFS ( persistent , source of truth )
Streaming Responses
Large files are streamed to avoid memory issues:
app . get ( '/api/workspace/:name/file/*' , async ( c ) => {
const filePath = c . req . param ( '*' );
// Stream large files
const stream = await workspace . createReadStream ( filePath );
return c . body ( stream , 200 );
});
Background Sync
Service workers enable background synchronization:
self . addEventListener ( 'sync' , ( event ) => {
if ( event . tag === 'sync-git' ) {
event . waitUntil ( syncWithGitRemote ());
}
});
Debugging
View service worker activity in Chrome DevTools:
Open DevTools → Application → Service Workers
Check “Update on reload” during development
Use “Unregister” to clear the service worker
Logging
Opal includes comprehensive service worker logging:
// From logger.ts
const logger = {
log : ( ... args ) => console . log ( '[SW]' , ... args ),
error : ( ... args ) => console . error ( '[SW]' , ... args ),
debug : ( ... args ) => console . debug ( '[SW]' , ... args )
};
Logs include:
Request routing
File operations
Performance metrics
Error details
Browser Compatibility
Service workers require:
HTTPS (or localhost for development)
Modern browser (Chrome 40+, Firefox 44+, Safari 11.1+, Edge 17+)
Opal gracefully degrades when service workers aren’t available:
// From BrowserAbility.ts
const canUseServiceWorker = async () => {
if ( ! ( 'serviceWorker' in navigator )) {
return false ;
}
try {
await setupServiceWorker ();
return true ;
} catch {
return false ;
}
};
Best Practices
Always call skipWaiting() and clients.claim() to ensure users get the latest version: self . addEventListener ( 'install' , ( event ) => {
event . waitUntil ( self . skipWaiting ());
});
Let the main app handle navigation requests: if ( request . mode === 'navigate' ) {
return fetch ( request );
}
Always provide fallbacks for failed operations: try {
return await handleRequest ( request );
} catch ( error ) {
return new Response ( 'Error' , { status: 500 });
}
Advanced Topics
Custom Request Handlers
Add custom routes to the service worker:
app . get ( '/api/custom/:param' , async ( c ) => {
const param = c . req . param ( 'param' );
// Custom logic
return c . json ({ result: 'custom' });
});
Cross-Origin Requests
Handle CORS for external resources:
app . use ( '*' , async ( c , next ) => {
await next ();
c . res . headers . set ( 'Access-Control-Allow-Origin' , '*' );
});
Message Passing
Communicate between main thread and service worker:
// Main thread
navigator . serviceWorker . controller . postMessage ({
type: 'CUSTOM_ACTION' ,
data: { ... }
});
// Service worker
self . addEventListener ( 'message' , ( event ) => {
if ( event . data . type === 'CUSTOM_ACTION' ) {
// Handle custom action
}
});