428 lines
13 KiB
Python
428 lines
13 KiB
Python
"""
|
|
Ritz Module System - Progressive Loading Concept
|
|
Split apps into multiple WASM modules for security and performance
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Set
|
|
|
|
@dataclass
|
|
class WASMModule:
|
|
"""Represents a WASM module"""
|
|
name: str
|
|
path: str
|
|
size_kb: int
|
|
dependencies: List[str]
|
|
exports: List[str]
|
|
requires_auth: bool = False
|
|
cache_strategy: str = 'aggressive' # aggressive, normal, no-cache
|
|
|
|
class ModuleGraph:
|
|
"""Dependency graph for WASM modules"""
|
|
|
|
def __init__(self):
|
|
self.modules: Dict[str, WASMModule] = {}
|
|
self.loaded: Set[str] = set()
|
|
|
|
def add_module(self, module: WASMModule):
|
|
self.modules[module.name] = module
|
|
|
|
def get_load_order(self, module_name: str) -> List[str]:
|
|
"""Get modules to load in dependency order"""
|
|
if module_name in self.loaded:
|
|
return []
|
|
|
|
order = []
|
|
visited = set()
|
|
|
|
def visit(name):
|
|
if name in visited or name in self.loaded:
|
|
return
|
|
visited.add(name)
|
|
|
|
module = self.modules.get(name)
|
|
if module:
|
|
for dep in module.dependencies:
|
|
visit(dep)
|
|
order.append(name)
|
|
|
|
visit(module_name)
|
|
return order
|
|
|
|
class RitzModuleSystem:
|
|
"""Progressive module loading system"""
|
|
|
|
def __init__(self):
|
|
self.graph = ModuleGraph()
|
|
self.setup_modules()
|
|
|
|
def setup_modules(self):
|
|
"""Define application module structure"""
|
|
|
|
# Bootstrap - minimal, always loaded
|
|
self.graph.add_module(WASMModule(
|
|
name='bootstrap',
|
|
path='/wasm/bootstrap.wasm',
|
|
size_kb=50,
|
|
dependencies=[],
|
|
exports=['LoginForm', 'ModuleLoader', 'AuthService'],
|
|
requires_auth=False,
|
|
cache_strategy='aggressive'
|
|
))
|
|
|
|
# Core app - loaded after authentication
|
|
self.graph.add_module(WASMModule(
|
|
name='app-core',
|
|
path='/wasm/app-core.wasm',
|
|
size_kb=200,
|
|
dependencies=['bootstrap'],
|
|
exports=['Router', 'Dashboard', 'Navigation'],
|
|
requires_auth=True,
|
|
cache_strategy='aggressive'
|
|
))
|
|
|
|
# Feature modules - lazy loaded
|
|
self.graph.add_module(WASMModule(
|
|
name='admin',
|
|
path='/wasm/features/admin.wasm',
|
|
size_kb=150,
|
|
dependencies=['app-core'],
|
|
exports=['AdminPanel', 'UserManagement', 'Settings'],
|
|
requires_auth=True,
|
|
cache_strategy='normal'
|
|
))
|
|
|
|
self.graph.add_module(WASMModule(
|
|
name='analytics',
|
|
path='/wasm/features/analytics.wasm',
|
|
size_kb=120,
|
|
dependencies=['app-core', 'charts'],
|
|
exports=['AnalyticsDashboard', 'Reports'],
|
|
requires_auth=True,
|
|
cache_strategy='normal'
|
|
))
|
|
|
|
self.graph.add_module(WASMModule(
|
|
name='charts',
|
|
path='/wasm/lib/charts.wasm',
|
|
size_kb=80,
|
|
dependencies=[],
|
|
exports=['LineChart', 'BarChart', 'PieChart'],
|
|
requires_auth=True,
|
|
cache_strategy='aggressive'
|
|
))
|
|
|
|
def generate_loader_js(self) -> str:
|
|
"""Generate JavaScript module loader"""
|
|
return '''
|
|
class RitzModuleLoader {
|
|
constructor() {
|
|
this.modules = new Map();
|
|
this.loading = new Map();
|
|
this.cache = caches.open('ritz-modules-v1');
|
|
}
|
|
|
|
async loadModule(name, config = {}) {
|
|
// Check if already loaded
|
|
if (this.modules.has(name)) {
|
|
return this.modules.get(name);
|
|
}
|
|
|
|
// Check if already loading (prevent double-load)
|
|
if (this.loading.has(name)) {
|
|
return this.loading.get(name);
|
|
}
|
|
|
|
// Start loading
|
|
const loadPromise = this._loadModule(name, config);
|
|
this.loading.set(name, loadPromise);
|
|
|
|
try {
|
|
const module = await loadPromise;
|
|
this.modules.set(name, module);
|
|
return module;
|
|
} finally {
|
|
this.loading.delete(name);
|
|
}
|
|
}
|
|
|
|
async _loadModule(name, config) {
|
|
const manifest = await this.getManifest();
|
|
const moduleInfo = manifest[name];
|
|
|
|
if (!moduleInfo) {
|
|
throw new Error(`Module ${name} not found`);
|
|
}
|
|
|
|
// Check authentication requirement
|
|
if (moduleInfo.requires_auth && !this.isAuthenticated()) {
|
|
throw new Error(`Module ${name} requires authentication`);
|
|
}
|
|
|
|
// Load dependencies first
|
|
for (const dep of moduleInfo.dependencies) {
|
|
await this.loadModule(dep, config);
|
|
}
|
|
|
|
// Try cache first
|
|
const cache = await this.cache;
|
|
const cached = await cache.match(moduleInfo.path);
|
|
|
|
let response;
|
|
if (cached && !config.bypassCache) {
|
|
console.log(`Loading ${name} from cache`);
|
|
response = cached;
|
|
} else {
|
|
console.log(`Downloading ${name} (${moduleInfo.size_kb}KB)`);
|
|
|
|
// Add auth token if required
|
|
const headers = {};
|
|
if (moduleInfo.requires_auth) {
|
|
headers['Authorization'] = `Bearer ${this.getAuthToken()}`;
|
|
}
|
|
|
|
response = await fetch(moduleInfo.path, { headers });
|
|
|
|
// Cache based on strategy
|
|
if (moduleInfo.cache_strategy !== 'no-cache') {
|
|
await cache.put(moduleInfo.path, response.clone());
|
|
}
|
|
}
|
|
|
|
// Instantiate WASM
|
|
const bytes = await response.arrayBuffer();
|
|
const wasm = await WebAssembly.instantiate(bytes, {
|
|
env: this.getImports(name),
|
|
...this.getModuleImports(moduleInfo.dependencies)
|
|
});
|
|
|
|
return wasm.instance;
|
|
}
|
|
|
|
getModuleImports(dependencies) {
|
|
// Link imported functions from dependencies
|
|
const imports = {};
|
|
for (const dep of dependencies) {
|
|
const module = this.modules.get(dep);
|
|
if (module) {
|
|
imports[dep] = module.exports;
|
|
}
|
|
}
|
|
return imports;
|
|
}
|
|
|
|
async preloadModules(names) {
|
|
// Preload modules in parallel
|
|
const promises = names.map(name => this.loadModule(name));
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
isAuthenticated() {
|
|
return localStorage.getItem('auth_token') !== null;
|
|
}
|
|
|
|
getAuthToken() {
|
|
return localStorage.getItem('auth_token');
|
|
}
|
|
|
|
async getManifest() {
|
|
// Module manifest with metadata
|
|
const response = await fetch('/wasm/manifest.json');
|
|
return response.json();
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const loader = new RitzModuleLoader();
|
|
|
|
// Bootstrap phase - no auth required
|
|
await loader.loadModule('bootstrap');
|
|
|
|
// After login
|
|
async function onLoginSuccess(token) {
|
|
localStorage.setItem('auth_token', token);
|
|
|
|
// Now load the main app
|
|
await loader.loadModule('app-core');
|
|
|
|
// Preload common features
|
|
loader.preloadModules(['charts']); // Non-blocking
|
|
|
|
// Navigate to dashboard
|
|
window.RitzApp.navigate('/dashboard');
|
|
}
|
|
|
|
// Lazy load on route change
|
|
window.addEventListener('route-change', async (e) => {
|
|
const route = e.detail.route;
|
|
|
|
if (route.startsWith('/admin')) {
|
|
await loader.loadModule('admin');
|
|
} else if (route.startsWith('/analytics')) {
|
|
await loader.loadModule('analytics');
|
|
}
|
|
});
|
|
'''
|
|
|
|
def generate_manifest(self) -> Dict:
|
|
"""Generate module manifest"""
|
|
manifest = {}
|
|
for name, module in self.graph.modules.items():
|
|
manifest[name] = {
|
|
'path': module.path,
|
|
'size_kb': module.size_kb,
|
|
'dependencies': module.dependencies,
|
|
'requires_auth': module.requires_auth,
|
|
'cache_strategy': module.cache_strategy,
|
|
'hash': self.calculate_hash(module)
|
|
}
|
|
return manifest
|
|
|
|
def calculate_hash(self, module: WASMModule) -> str:
|
|
"""Calculate hash for cache invalidation"""
|
|
content = f"{module.name}:{module.path}:{module.size_kb}"
|
|
return hashlib.sha256(content.encode()).hexdigest()[:12]
|
|
|
|
def split_components_into_modules(self, components: List[str]) -> Dict[str, List[str]]:
|
|
"""Automatically split components into modules"""
|
|
|
|
# Example heuristic: group by feature area
|
|
modules = {
|
|
'bootstrap': [],
|
|
'app-core': [],
|
|
'admin': [],
|
|
'analytics': [],
|
|
'reports': []
|
|
}
|
|
|
|
for component in components:
|
|
if component.startswith('Login') or component.startswith('Auth'):
|
|
modules['bootstrap'].append(component)
|
|
elif component.startswith('Admin'):
|
|
modules['admin'].append(component)
|
|
elif component.startswith('Analytics') or component.startswith('Chart'):
|
|
modules['analytics'].append(component)
|
|
elif component.startswith('Report'):
|
|
modules['reports'].append(component)
|
|
else:
|
|
modules['app-core'].append(component)
|
|
|
|
# Remove empty modules
|
|
return {k: v for k, v in modules.items() if v}
|
|
|
|
class SecurityExample:
|
|
"""Security benefits of modular loading"""
|
|
|
|
@staticmethod
|
|
def bootstrap_only_code():
|
|
"""Code that ships to all users (even unauthenticated)"""
|
|
return '''
|
|
# bootstrap.py - Compiles to bootstrap.wasm
|
|
@component
|
|
def LoginPage():
|
|
username = signal('')
|
|
password = signal('')
|
|
error = signal(None)
|
|
|
|
async def handle_login():
|
|
try:
|
|
response = await fetch('/api/login', {
|
|
'method': 'POST',
|
|
'body': json.dumps({
|
|
'username': username.value,
|
|
'password': password.value
|
|
})
|
|
})
|
|
|
|
if response.ok:
|
|
token = await response.json()
|
|
# Now load the real app
|
|
await ModuleLoader.load('app-core')
|
|
navigate('/dashboard')
|
|
else:
|
|
error.value = 'Invalid credentials'
|
|
except Exception as e:
|
|
error.value = str(e)
|
|
|
|
with Main():
|
|
with Form(onsubmit=handle_login):
|
|
yield H1('Login')
|
|
yield Input(
|
|
type='text',
|
|
placeholder='Username',
|
|
value=username,
|
|
oninput=lambda e: username.set(e.target.value)
|
|
)
|
|
yield Input(
|
|
type='password',
|
|
placeholder='Password',
|
|
value=password,
|
|
oninput=lambda e: password.set(e.target.value)
|
|
)
|
|
yield Button('Sign In', type='submit')
|
|
|
|
if error.value:
|
|
yield ErrorMessage(error.value)
|
|
|
|
# IMPORTANT: No business logic, no API endpoints, no sensitive data!
|
|
# This is ALL that unauthenticated users can see
|
|
'''
|
|
|
|
@staticmethod
|
|
def protected_module_code():
|
|
"""Code that only authenticated users receive"""
|
|
return '''
|
|
# app-core.py - Compiles to app-core.wasm
|
|
# This is NEVER sent to unauthenticated users
|
|
|
|
@component
|
|
def Dashboard():
|
|
# Business logic here
|
|
user_data = signal(None)
|
|
|
|
async def load_user_data():
|
|
# API endpoints only known to authenticated users
|
|
response = await fetch('/api/user/profile')
|
|
user_data.value = await response.json()
|
|
|
|
# ... rest of application
|
|
'''
|
|
|
|
# Demonstration
|
|
if __name__ == '__main__':
|
|
print("=== Ritz Module System ===\n")
|
|
|
|
system = RitzModuleSystem()
|
|
|
|
print("1. Module Structure:")
|
|
for name, module in system.graph.modules.items():
|
|
print(f" {name}:")
|
|
print(f" Size: {module.size_kb}KB")
|
|
print(f" Auth Required: {module.requires_auth}")
|
|
print(f" Dependencies: {module.dependencies}")
|
|
print(f" Exports: {module.exports[:2]}...")
|
|
|
|
print("\n2. Load Order for 'analytics':")
|
|
order = system.graph.get_load_order('analytics')
|
|
print(f" Must load in order: {order}")
|
|
total_size = sum(system.graph.modules[m].size_kb for m in order)
|
|
print(f" Total download: {total_size}KB")
|
|
|
|
print("\n3. Module Manifest:")
|
|
manifest = system.generate_manifest()
|
|
print(json.dumps(manifest, indent=2)[:400] + "...")
|
|
|
|
print("\n4. Security Benefits:")
|
|
print(" - Unauthenticated users only get bootstrap.wasm (50KB)")
|
|
print(" - No API endpoints or business logic exposed")
|
|
print(" - Feature modules loaded on-demand")
|
|
print(" - Each module can have different cache strategies")
|
|
|
|
print("\n5. Performance Benefits:")
|
|
print(" - Initial load: 50KB instead of 600KB+")
|
|
print(" - Aggressive caching for stable modules")
|
|
print(" - Parallel preloading of likely-needed modules")
|
|
print(" - Route-based code splitting")
|