Files
strata-compile/runtime/fetch/fetch.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

234 lines
6.3 KiB
Plaintext

/**
* 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);
}
}
}
}
}