- 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
261 lines
6.8 KiB
Plaintext
261 lines
6.8 KiB
Plaintext
/**
|
|
* 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;
|
|
}
|