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

104 lines
2.4 KiB
TypeScript
Raw Normal View History

2025-09-01 19:01:01 -05:00
import { produce } from "immer";
import { merge } from "lodash-es";
import { useEffect, useState } from "react";
2025-09-01 18:03:07 -05:00
2025-09-01 18:51:04 -05:00
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),
2025-09-01 19:01:01 -05:00
private persistence?: string
2025-09-01 18:03:07 -05:00
) {
2025-09-01 19:01:01 -05:00
if (typeof creator !== "function") this.initialize(creator);
2025-09-01 18:51:04 -05:00
}
private initialize(initial: Type) {
let data = initial;
2025-09-01 19:16:36 -05:00
if (this.persistence && typeof localStorage !== "undefined") {
2025-09-01 18:51:04 -05:00
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
}
2025-09-01 19:01:01 -05:00
useInitialization(...args: Arguments[]) {
if (this.initialized) return;
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));
}
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) {
2025-09-01 22:55:31 -05:00
let last = slicer(this.state);
2025-09-01 18:03:07 -05:00
function listener(state: Type) {
const next = slicer(state);
2025-09-01 22:55:31 -05:00
if (next !== last) {
callback(next);
last = next;
}
2025-09-01 18:03:07 -05:00
}
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() {
2025-09-01 19:01:01 -05:00
if (!this.initialized) throw new Error("State used before initialization");
2025-09-01 18:51:04 -05:00
}
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>(
2025-09-01 19:01:01 -05:00
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
2025-09-01 18:13:41 -05:00
) {
2025-09-01 22:55:31 -05:00
const instance = this;
2025-09-01 18:03:07 -05:00
const [slice, setSlice] = useState(slicer(this.state));
2025-09-01 22:55:31 -05:00
useEffect(() => instance.on(slicer, setSlice), []);
2025-09-01 18:03:07 -05:00
return slice;
}
}