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
This commit is contained in:
2026-01-16 09:01:29 -05:00
commit 9e451469f5
48 changed files with 15605 additions and 0 deletions

231
runtime/core/strata.sts Normal file
View File

@@ -0,0 +1,231 @@
/**
* Strata Core Runtime
* Main entry point for browser runtime
*/
import { StrataStore, createStore, useStore } from '../store/store.sts';
import { StrataFetch } from '../fetch/fetch.sts';
interface StrataConfig {
encryptionKey?: number[];
apiBaseUrl?: string;
devMode?: boolean;
}
interface BroadcastHandler {
(data: any): void;
}
class Strata {
private worker: SharedWorker | null = null;
private _tabId: string = '';
private messageHandlers: Map<string, Function[]> = new Map();
private broadcastHandlers: Map<string, BroadcastHandler[]> = new Map();
private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
private config: StrataConfig = {};
private _fetch: StrataFetch | null = null;
get tabId(): string {
return this._tabId;
}
get fetch(): StrataFetch {
if (!this._fetch) {
throw new Error('Strata not initialized. Call strata.init() first.');
}
return this._fetch;
}
async init(config: StrataConfig = {}): Promise<void> {
this.config = config;
// Initialize SharedWorker
if (typeof SharedWorker !== 'undefined') {
this.worker = new SharedWorker(
new URL('../worker/shared-worker.sts', import.meta.url),
{ type: 'module', name: 'strata-worker' }
);
this.worker.port.onmessage = (event) => this.handleWorkerMessage(event.data);
this.worker.port.start();
// Send encryption key if provided
if (config.encryptionKey) {
this.worker.port.postMessage({
type: 'setEncryptionKey',
key: config.encryptionKey,
});
}
// Wait for connection
await this.waitForConnection();
} else {
// Fallback for browsers without SharedWorker support
this._tabId = 'tab_' + Math.random().toString(36).slice(2, 10);
console.warn('SharedWorker not supported, falling back to single-tab mode');
}
// Initialize fetch
this._fetch = new StrataFetch(this);
// Handle page unload
window.addEventListener('beforeunload', () => {
this.disconnect();
});
if (config.devMode) {
(window as any).__STRATA__ = this;
console.log(`Strata initialized (tabId: ${this._tabId})`);
}
}
private waitForConnection(): Promise<void> {
return new Promise((resolve) => {
const handler = (message: any) => {
if (message.type === 'connected') {
this._tabId = message.tabId;
resolve();
}
};
this.addMessageHandler('connected', handler);
});
}
private handleWorkerMessage(message: any) {
// Handle specific message types
switch (message.type) {
case 'connected':
this._tabId = message.tabId;
break;
case 'fetch:response':
case 'fetch:error':
this.handleFetchResponse(message);
break;
case 'store:value':
this.handleStoreValue(message);
break;
case 'broadcast':
this.handleBroadcast(message);
break;
}
// Call registered handlers
const handlers = this.messageHandlers.get(message.type) || [];
for (const handler of handlers) {
handler(message);
}
}
private handleFetchResponse(message: any) {
const pending = this.pendingRequests.get(message.requestId);
if (pending) {
if (message.type === 'fetch:error') {
pending.reject(new Error(message.error));
} else {
pending.resolve(message.data);
}
this.pendingRequests.delete(message.requestId);
}
}
private handleStoreValue(message: any) {
// Handled by store subscriptions
}
private handleBroadcast(message: any) {
const handlers = this.broadcastHandlers.get(message.event) || [];
for (const handler of handlers) {
handler(message.data);
}
// Also call wildcard handlers
const wildcardHandlers = this.broadcastHandlers.get('*') || [];
for (const handler of wildcardHandlers) {
handler({ event: message.event, data: message.data });
}
}
private addMessageHandler(type: string, handler: Function) {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type)!.push(handler);
}
// Public API
sendToWorker(message: any): void {
if (this.worker) {
this.worker.port.postMessage(message);
}
}
async fetchViaWorker(url: string, options?: any): Promise<any> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
this.pendingRequests.set(requestId, { resolve, reject });
this.sendToWorker({
type: 'fetch',
url,
options,
requestId,
});
// Timeout after 30 seconds
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
broadcast(event: string, data?: any, targetTabs?: string[]): void {
this.sendToWorker({
type: 'broadcast',
event,
data,
targetTabs,
});
}
onBroadcast(event: string, handler: BroadcastHandler): () => void {
if (!this.broadcastHandlers.has(event)) {
this.broadcastHandlers.set(event, []);
}
this.broadcastHandlers.get(event)!.push(handler);
// Return unsubscribe function
return () => {
const handlers = this.broadcastHandlers.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
};
}
invalidateCache(pattern: string = '*'): void {
this.sendToWorker({
type: 'cache:invalidate',
pattern,
});
}
private disconnect(): void {
this.sendToWorker({ type: 'disconnect' });
}
}
// Singleton instance
export const strata = new Strata();
// Re-exports
export { createStore, useStore };
export type { StrataStore };

233
runtime/fetch/fetch.sts Normal file
View File

@@ -0,0 +1,233 @@
/**
* Strata Smart Fetch
* Deduplication, caching, and no re-render for unchanged data
*/
import type { Strata } from '../core/strata.sts';
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
body?: any;
cache?: 'none' | 'smart' | 'permanent' | string; // string for TTL like '5m', '1h'
stale?: string; // stale-while-revalidate TTL
dedupe?: boolean; // default true
transform?: (data: any) => any;
}
interface QueryState<T> {
data: T | null;
loading: boolean;
error: Error | null;
isStale: boolean;
}
type QuerySubscriber<T> = (state: QueryState<T>) => void;
export class StrataFetch {
private strata: any; // Strata instance
private queryCache: Map<string, QueryState<any>> = new Map();
private subscribers: Map<string, Set<QuerySubscriber<any>>> = new Map();
constructor(strata: any) {
this.strata = strata;
}
/**
* Basic fetch with deduplication and caching
*/
async fetch<T = any>(url: string, options: FetchOptions = {}): Promise<T> {
const fullUrl = this.resolveUrl(url);
const data = await this.strata.fetchViaWorker(fullUrl, {
method: options.method || 'GET',
headers: options.headers,
body: options.body,
cache: options.cache || 'smart',
});
return options.transform ? options.transform(data) : data;
}
/**
* GET request
*/
async get<T = any>(url: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'GET' });
}
/**
* POST request
*/
async post<T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'POST', body });
}
/**
* PUT request
*/
async put<T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'PUT', body });
}
/**
* DELETE request
*/
async delete<T = any>(url: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
return this.fetch<T>(url, { ...options, method: 'DELETE' });
}
/**
* Reactive query hook - only re-renders when data changes
*/
useQuery<T = any>(
url: string,
options: FetchOptions = {}
): {
subscribe: (subscriber: QuerySubscriber<T>) => () => void;
refetch: () => Promise<void>;
getData: () => QueryState<T>;
} {
const cacheKey = this.getCacheKey(url, options);
// Initialize state if not exists
if (!this.queryCache.has(cacheKey)) {
this.queryCache.set(cacheKey, {
data: null,
loading: true,
error: null,
isStale: false,
});
// Start initial fetch
this.executeQuery(url, options, cacheKey);
}
return {
subscribe: (subscriber: QuerySubscriber<T>) => {
if (!this.subscribers.has(cacheKey)) {
this.subscribers.set(cacheKey, new Set());
}
this.subscribers.get(cacheKey)!.add(subscriber);
// Immediately call with current state
subscriber(this.queryCache.get(cacheKey)!);
// Return unsubscribe
return () => {
this.subscribers.get(cacheKey)?.delete(subscriber);
};
},
refetch: () => this.executeQuery(url, options, cacheKey),
getData: () => this.queryCache.get(cacheKey)!,
};
}
private async executeQuery(url: string, options: FetchOptions, cacheKey: string): Promise<void> {
const currentState = this.queryCache.get(cacheKey)!;
// Set loading (but keep old data for stale-while-revalidate)
this.updateQueryState(cacheKey, {
...currentState,
loading: true,
isStale: currentState.data !== null,
});
try {
const newData = await this.fetch(url, options);
// Deep compare with existing data
const hasChanged = !this.deepEqual(currentState.data, newData);
if (hasChanged) {
this.updateQueryState(cacheKey, {
data: newData,
loading: false,
error: null,
isStale: false,
});
} else {
// Data unchanged - just update loading state, no re-render trigger
const state = this.queryCache.get(cacheKey)!;
state.loading = false;
state.isStale = false;
// Don't notify subscribers since data didn't change
}
} catch (error) {
this.updateQueryState(cacheKey, {
...currentState,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
isStale: false,
});
}
}
private updateQueryState<T>(cacheKey: string, state: QueryState<T>): void {
this.queryCache.set(cacheKey, state);
// Notify subscribers
const subs = this.subscribers.get(cacheKey);
if (subs) {
for (const subscriber of subs) {
subscriber(state);
}
}
}
private getCacheKey(url: string, options: FetchOptions): string {
const method = options.method || 'GET';
const body = options.body ? JSON.stringify(options.body) : '';
return `${method}:${url}:${body}`;
}
private resolveUrl(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Use base URL from config if available
const baseUrl = (window as any).__STRATA_CONFIG__?.apiBaseUrl || '';
return baseUrl + url;
}
private deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!this.deepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}
/**
* Invalidate cache for specific URLs
*/
invalidate(pattern?: string): void {
this.strata.invalidateCache(pattern || '*');
// Also clear local query cache
if (!pattern || pattern === '*') {
this.queryCache.clear();
} else {
for (const key of this.queryCache.keys()) {
if (key.includes(pattern)) {
this.queryCache.delete(key);
}
}
}
}
}

260
runtime/store/store.sts Normal file
View File

@@ -0,0 +1,260 @@
/**
* Strata Store
* Fast state management with encryption and cross-tab sync
*/
import { strata } from '../core/strata.sts';
type Subscriber<T> = (state: T, prevState: T) => void;
type Selector<T, R> = (state: T) => R;
interface StoreOptions<T> {
state: T;
actions?: Record<string, (this: T & StoreActions<T>, ...args: any[]) => any>;
encrypt?: boolean | string[]; // true = all, string[] = specific fields
persist?: boolean | string[]; // true = all, string[] = specific fields
shared?: boolean | string[]; // true = all, string[] = specific fields across tabs
}
interface StoreActions<T> {
$set: (partial: Partial<T>) => void;
$reset: () => void;
$subscribe: (subscriber: Subscriber<T>) => () => void;
}
export interface StrataStore<T> extends StoreActions<T> {
getState: () => T;
setState: (partial: Partial<T>) => void;
}
const stores: Map<string, StrataStore<any>> = new Map();
/**
* Create a new store
*/
export function createStore<T extends object>(
name: string,
options: StoreOptions<T>
): StrataStore<T> & T {
if (stores.has(name)) {
return stores.get(name) as StrataStore<T> & T;
}
const initialState = { ...options.state };
let state = { ...initialState };
const subscribers: Set<Subscriber<T>> = new Set();
// Track which fields should be encrypted/persisted/shared
const encryptFields = normalizeFields(options.encrypt);
const persistFields = normalizeFields(options.persist);
const sharedFields = normalizeFields(options.shared);
function normalizeFields(opt: boolean | string[] | undefined): Set<string> | 'all' | null {
if (opt === true) return 'all';
if (Array.isArray(opt)) return new Set(opt);
return null;
}
function shouldEncrypt(field: string): boolean {
if (encryptFields === 'all') return true;
if (encryptFields instanceof Set) return encryptFields.has(field);
return false;
}
function shouldPersist(field: string): boolean {
if (persistFields === 'all') return true;
if (persistFields instanceof Set) return persistFields.has(field);
return false;
}
function shouldShare(field: string): boolean {
if (sharedFields === 'all') return true;
if (sharedFields instanceof Set) return sharedFields.has(field);
return false;
}
function notify(prevState: T) {
for (const subscriber of subscribers) {
subscriber(state, prevState);
}
}
function setState(partial: Partial<T>) {
const prevState = { ...state };
const changes = Object.entries(partial);
for (const [key, value] of changes) {
(state as any)[key] = value;
// Sync to SharedWorker
if (shouldPersist(key) || shouldShare(key)) {
strata.sendToWorker({
type: 'store:set',
storeName: name,
key,
value,
scope: shouldShare(key) ? 'shared' : 'tab',
encrypt: shouldEncrypt(key),
});
}
}
notify(prevState);
}
function getState(): T {
return { ...state };
}
// Core store object
const store: StrataStore<T> = {
getState,
setState,
$set: setState,
$reset: () => {
setState(initialState as Partial<T>);
},
$subscribe: (subscriber: Subscriber<T>) => {
subscribers.add(subscriber);
return () => subscribers.delete(subscriber);
},
};
// Bind actions
if (options.actions) {
for (const [actionName, actionFn] of Object.entries(options.actions)) {
(store as any)[actionName] = function (...args: any[]) {
return actionFn.apply(
new Proxy(state, {
set(target, prop, value) {
setState({ [prop]: value } as Partial<T>);
return true;
},
get(target, prop) {
if (prop === '$set') return setState;
if (prop === '$reset') return store.$reset;
return (target as any)[prop];
},
}),
args
);
};
}
}
// Create proxy for direct property access
const proxy = new Proxy(store as StrataStore<T> & T, {
get(target, prop: string) {
if (prop in target) return (target as any)[prop];
return state[prop as keyof T];
},
set(target, prop: string, value) {
if (prop in state) {
setState({ [prop]: value } as Partial<T>);
return true;
}
return false;
},
});
// Listen for cross-tab updates
strata.onBroadcast('store:changed', (data: any) => {
if (data.storeName === name && shouldShare(data.key)) {
const prevState = { ...state };
(state as any)[data.key] = data.value;
notify(prevState);
}
});
// Load persisted state
loadPersistedState(name, state, persistFields, sharedFields);
stores.set(name, proxy);
return proxy;
}
/**
* Hook to use store with optional selector
*/
export function useStore<T, R = T>(
store: StrataStore<T>,
selector?: Selector<T, R>
): R {
const state = store.getState();
return selector ? selector(state) : (state as unknown as R);
}
/**
* Subscribe to store changes with selector
*/
export function subscribeToStore<T, R>(
store: StrataStore<T>,
selector: Selector<T, R>,
callback: (value: R, prevValue: R) => void
): () => void {
let prevValue = selector(store.getState());
return store.$subscribe((state, prevState) => {
const newValue = selector(state);
const oldValue = selector(prevState);
// Only call if selected value changed
if (!deepEqual(newValue, oldValue)) {
callback(newValue, prevValue);
prevValue = newValue;
}
});
}
async function loadPersistedState<T>(
storeName: string,
state: T,
persistFields: Set<string> | 'all' | null,
sharedFields: Set<string> | 'all' | null
): Promise<void> {
// Load from SharedWorker on init
const fields = new Set<string>();
if (persistFields === 'all' || sharedFields === 'all') {
Object.keys(state as object).forEach((k) => fields.add(k));
} else {
if (persistFields instanceof Set) persistFields.forEach((k) => fields.add(k));
if (sharedFields instanceof Set) sharedFields.forEach((k) => fields.add(k));
}
// Request each field from worker
// (In production, batch this into single request)
for (const field of fields) {
strata.sendToWorker({
type: 'store:get',
storeName,
key: field,
scope: sharedFields === 'all' || (sharedFields instanceof Set && sharedFields.has(field))
? 'shared'
: 'tab',
});
}
}
function deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!deepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}

View File

@@ -0,0 +1,392 @@
/**
* 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();