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:
260
runtime/store/store.sts
Normal file
260
runtime/store/store.sts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user