Files
strata-compile/runtime/worker/shared-worker.sts
Carlos Gutierrez 9e451469f5 git commit -m "feat: initial release of Strata framework v0.1.0
- Static compiler with STRC pattern (Static Template Resolution with
   Compartmentalized Layers)
   - Template syntax: { } interpolation, { s-for }, { s-if/s-elif/s-else
    }
   - File types: .strata, .compiler.sts, .service.sts, .api.sts, .sts,
   .scss
   - CLI tools: strata dev, strata build, strata g (generators)
   - create-strata scaffolding CLI with Pokemon API example
   - Dev server with WebSocket HMR (Hot Module Replacement)
   - Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
   LICENSE
2026-01-16 09:01:29 -05:00

393 lines
9.8 KiB
Plaintext

/**
* Strata Shared Worker
* Manages cross-tab state, caching, and API deduplication
*/
interface TabConnection {
port: MessagePort;
tabId: string;
connectedAt: number;
lastActivity: number;
}
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
etag?: string;
}
interface InFlightRequest {
promise: Promise<any>;
subscribers: string[]; // tabIds waiting for this request
}
interface StoreState {
[storeName: string]: {
shared: Record<string, any>;
tabs: Record<string, Record<string, any>>;
};
}
class StrataSharedWorker {
private tabs: Map<string, TabConnection> = new Map();
private cache: Map<string, CacheEntry> = new Map();
private inFlight: Map<string, InFlightRequest> = new Map();
private stores: StoreState = {};
private encryptionKey: Uint8Array | null = null;
constructor() {
self.onconnect = (e: MessageEvent) => this.handleConnect(e);
}
private generateTabId(): string {
return 'tab_' + crypto.randomUUID().slice(0, 8);
}
private handleConnect(e: MessageEvent) {
const port = e.ports[0];
const tabId = this.generateTabId();
const connection: TabConnection = {
port,
tabId,
connectedAt: Date.now(),
lastActivity: Date.now(),
};
this.tabs.set(tabId, connection);
port.onmessage = (event) => this.handleMessage(tabId, event.data);
port.start();
// Send tabId to the new tab
port.postMessage({
type: 'connected',
tabId,
tabCount: this.tabs.size,
});
// Notify other tabs
this.broadcast('tab:joined', { tabId, tabCount: this.tabs.size }, [tabId]);
}
private handleMessage(tabId: string, message: any) {
const tab = this.tabs.get(tabId);
if (tab) {
tab.lastActivity = Date.now();
}
switch (message.type) {
case 'fetch':
this.handleFetch(tabId, message);
break;
case 'store:get':
this.handleStoreGet(tabId, message);
break;
case 'store:set':
this.handleStoreSet(tabId, message);
break;
case 'store:subscribe':
this.handleStoreSubscribe(tabId, message);
break;
case 'broadcast':
this.handleBroadcast(tabId, message);
break;
case 'cache:invalidate':
this.handleCacheInvalidate(message);
break;
case 'disconnect':
this.handleDisconnect(tabId);
break;
case 'setEncryptionKey':
this.setEncryptionKey(message.key);
break;
}
}
private async handleFetch(tabId: string, message: any) {
const { url, options, requestId } = message;
const cacheKey = this.getCacheKey(url, options);
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached && !this.isCacheExpired(cached)) {
this.sendToTab(tabId, {
type: 'fetch:response',
requestId,
data: cached.data,
fromCache: true,
});
return;
}
// Check if request is already in flight
const inFlight = this.inFlight.get(cacheKey);
if (inFlight) {
// Add this tab to subscribers
inFlight.subscribers.push(tabId);
const data = await inFlight.promise;
this.sendToTab(tabId, {
type: 'fetch:response',
requestId,
data,
deduplicated: true,
});
return;
}
// Make the request
const fetchPromise = this.executeFetch(url, options);
this.inFlight.set(cacheKey, {
promise: fetchPromise,
subscribers: [tabId],
});
try {
const data = await fetchPromise;
const inFlightEntry = this.inFlight.get(cacheKey);
// Cache the response
if (options?.cache !== 'none') {
const ttl = this.parseTTL(options?.cache || 'smart');
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
ttl,
});
}
// Send to all subscribers
if (inFlightEntry) {
for (const subTabId of inFlightEntry.subscribers) {
this.sendToTab(subTabId, {
type: 'fetch:response',
requestId,
data,
fromCache: false,
});
}
}
} catch (error) {
const inFlightEntry = this.inFlight.get(cacheKey);
if (inFlightEntry) {
for (const subTabId of inFlightEntry.subscribers) {
this.sendToTab(subTabId, {
type: 'fetch:error',
requestId,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
} finally {
this.inFlight.delete(cacheKey);
}
}
private async executeFetch(url: string, options: any): Promise<any> {
const response = await fetch(url, {
method: options?.method || 'GET',
headers: options?.headers,
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
private getCacheKey(url: string, options: any): string {
const method = options?.method || 'GET';
const body = options?.body ? JSON.stringify(options.body) : '';
return `${method}:${url}:${body}`;
}
private isCacheExpired(entry: CacheEntry): boolean {
if (entry.ttl === -1) return false; // permanent
return Date.now() - entry.timestamp > entry.ttl;
}
private parseTTL(cache: string): number {
if (cache === 'permanent') return -1;
if (cache === 'none') return 0;
if (cache === 'smart') return 5 * 60 * 1000; // 5 minutes default
const match = cache.match(/^(\d+)(s|m|h|d)$/);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
const multipliers: Record<string, number> = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
};
return value * multipliers[unit];
}
return 5 * 60 * 1000; // default 5 minutes
}
private handleStoreGet(tabId: string, message: any) {
const { storeName, key, scope } = message;
const store = this.stores[storeName];
if (!store) {
this.sendToTab(tabId, {
type: 'store:value',
storeName,
key,
value: undefined,
});
return;
}
let value;
if (scope === 'tab') {
value = store.tabs[tabId]?.[key];
} else {
value = store.shared[key];
}
// Decrypt if needed
if (this.encryptionKey && value) {
value = this.decrypt(value);
}
this.sendToTab(tabId, {
type: 'store:value',
storeName,
key,
value,
});
}
private handleStoreSet(tabId: string, message: any) {
const { storeName, key, value, scope, encrypt } = message;
if (!this.stores[storeName]) {
this.stores[storeName] = { shared: {}, tabs: {} };
}
let storedValue = value;
if (encrypt && this.encryptionKey) {
storedValue = this.encrypt(value);
}
if (scope === 'tab') {
if (!this.stores[storeName].tabs[tabId]) {
this.stores[storeName].tabs[tabId] = {};
}
this.stores[storeName].tabs[tabId][key] = storedValue;
} else {
this.stores[storeName].shared[key] = storedValue;
// Notify all tabs about shared state change
this.broadcast('store:changed', {
storeName,
key,
value,
});
}
}
private handleStoreSubscribe(tabId: string, message: any) {
// Store subscriptions are handled via broadcast
// When store changes, all tabs receive updates
}
private handleBroadcast(fromTabId: string, message: any) {
const { event, data, targetTabs } = message;
this.broadcast(event, data, targetTabs ? undefined : [fromTabId]);
}
private handleCacheInvalidate(message: any) {
const { pattern } = message;
if (pattern === '*') {
this.cache.clear();
} else {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
private handleDisconnect(tabId: string) {
this.tabs.delete(tabId);
// Clean up tab-specific store data
for (const storeName in this.stores) {
delete this.stores[storeName].tabs[tabId];
}
// Notify remaining tabs
this.broadcast('tab:left', { tabId, tabCount: this.tabs.size });
}
private broadcast(event: string, data: any, excludeTabs: string[] = []) {
for (const [tabId, tab] of this.tabs) {
if (!excludeTabs.includes(tabId)) {
tab.port.postMessage({
type: 'broadcast',
event,
data,
});
}
}
}
private sendToTab(tabId: string, message: any) {
const tab = this.tabs.get(tabId);
if (tab) {
tab.port.postMessage(message);
}
}
private setEncryptionKey(keyArray: number[]) {
this.encryptionKey = new Uint8Array(keyArray);
}
private encrypt(data: any): string {
if (!this.encryptionKey) return JSON.stringify(data);
const jsonStr = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBytes = encoder.encode(jsonStr);
// XOR encryption with key (simple, fast)
// In production, use SubtleCrypto AES-GCM
const encrypted = new Uint8Array(dataBytes.length);
for (let i = 0; i < dataBytes.length; i++) {
encrypted[i] = dataBytes[i] ^ this.encryptionKey[i % this.encryptionKey.length];
}
return btoa(String.fromCharCode(...encrypted));
}
private decrypt(encryptedStr: string): any {
if (!this.encryptionKey) return JSON.parse(encryptedStr);
const encrypted = new Uint8Array(
atob(encryptedStr)
.split('')
.map((c) => c.charCodeAt(0))
);
const decrypted = new Uint8Array(encrypted.length);
for (let i = 0; i < encrypted.length; i++) {
decrypted[i] = encrypted[i] ^ this.encryptionKey[i % this.encryptionKey.length];
}
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
}
// Initialize worker
new StrataSharedWorker();