Files
blitzkit/packages/varuna/src/index.ts
T

101 lines
2.3 KiB
TypeScript
Raw Normal View History

2025-09-01 18:03:07 -05:00
import { produce } from 'immer';
import { merge } from 'lodash-es';
import { useEffect, useState } from 'react';
2025-09-01 18:51:04 -05:00
interface ProviderProps<Arguments> {
args: Arguments;
}
export class Varuna<Type, Arguments = void> {
2025-09-01 18:03:07 -05:00
private listeners = new Set<(state: Type) => void>();
2025-09-01 18:51:04 -05:00
private initialized = false;
private _initial?: Type;
private _state?: Type;
2025-09-01 18:03:07 -05:00
constructor(
2025-09-01 18:51:04 -05:00
private creator: Type | ((...args: Arguments[]) => Type),
private persistence?: string,
2025-09-01 18:03:07 -05:00
) {
2025-09-01 18:51:04 -05:00
if (typeof creator !== 'function') this.initialize(creator);
}
Provider({ args }: ProviderProps<Arguments>) {
if (typeof this.creator !== 'function') {
throw new Error('Provider must be used with a function creator');
}
this.initialize((this.creator as (...args: Arguments[]) => Type)(args));
return null;
}
private initialize(initial: Type) {
let data = initial;
if (this.persistence) {
const dehydrated = localStorage.getItem(this.persistence);
2025-09-01 18:03:07 -05:00
if (dehydrated) {
const rehydrated = JSON.parse(dehydrated) as unknown;
2025-09-01 18:51:04 -05:00
data = merge(data, rehydrated);
2025-09-01 18:03:07 -05:00
}
}
2025-09-01 18:51:04 -05:00
this.initialized = true;
this._initial = data;
this._state = data;
2025-09-01 18:03:07 -05:00
}
private dispatch() {
const state = this.state;
this.listeners.forEach((callback) => callback(state));
}
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
const last = slicer(this.state);
function listener(state: Type) {
const next = slicer(state);
if (next !== last) callback(next);
}
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
mutate(mutator: (state: Type) => void) {
2025-09-01 18:13:41 -05:00
this.set(produce(this.state, mutator));
}
2025-09-01 18:51:04 -05:00
private assertInitialized() {
if (!this.initialized) throw new Error('State used before initialization');
}
get state() {
this.assertInitialized();
return this._state as Type;
}
2025-09-01 18:13:41 -05:00
set(state: Type) {
2025-09-01 18:51:04 -05:00
this.assertInitialized();
this._state = state;
2025-09-01 18:03:07 -05:00
this.dispatch();
}
2025-09-01 18:51:04 -05:00
get initial() {
this.assertInitialized();
return this._initial as Type;
}
2025-09-01 18:13:41 -05:00
use<Slice = Type>(
slicer: (state: Type) => Slice = (state) => state as unknown as Slice,
) {
2025-09-01 18:03:07 -05:00
const [slice, setSlice] = useState(slicer(this.state));
useEffect(() => this.on(slicer, setSlice), []);
return slice;
}
}