Last active
November 11, 2024 23:52
-
-
Save beorn/23c8bee75ac2acd4758ac0d3defe76e7 to your computer and use it in GitHub Desktop.
Revisions
-
beorn revised this gist
Nov 11, 2024 . 1 changed file with 9 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,4 @@ // NOTE: I did use Cursor/LLMs to help write this import React from "react" /** Base class for reactive values that can notify subscribers of changes */ @@ -118,9 +117,10 @@ type ComputedState<T> = { : never } // Update Store type to handle methods as Observable<Function> type Store<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? Observable<(...args: Parameters<T[K]>) => void> : Observable<T[K]> } @@ -170,13 +170,16 @@ export function createStore<T extends Record<string, any>>( for (const key in definition) { const value = definition[key] if (typeof value !== "function" || computeds.has(key as keyof T)) continue // Wrap method in Observable const method = (...args: any[]) => { const updates = value.apply(state, args) as Partial<T> if (!updates) return Object.entries(updates).forEach(([key, value]) => { if (key in state) (state[key as keyof T] as any) = value }) } store[key] = new Observable(method) } return store as Store<StoreDefinition<T>> @@ -194,6 +197,7 @@ export default function MyComponent() { const b = useStoreValue(store.b) const sum = useStoreValue(store.sum) const sum2x = useStoreValue(store.sum2x) const addA = useStoreValue(store.addA) return ( <div> @@ -202,7 +206,7 @@ export default function MyComponent() { <div>b: {b}</div> <div>sum: {sum}</div> <div>sum2x: {sum2x}</div> <button onClick={() => addA(5)}>Add to A</button> </div> ) } -
beorn revised this gist
Nov 11, 2024 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ // NOTE: I did use Cursor/LLMs to help write this import React from "react" /** Base class for reactive values that can notify subscribers of changes */ -
beorn created this gist
Nov 11, 2024 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,220 @@ import React from "react" /** Base class for reactive values that can notify subscribers of changes */ class Observable<T> { protected _value: T protected subscribers = new Set<() => void>() /** Tracks dependencies during computed property evaluation */ static currDeps: Set<Observable<any>> | null = null constructor(value: T) { this._value = value } /** Get current value and register as dependency if being tracked */ get value(): T { if (Observable.currDeps) Observable.currDeps.add(this) return this._value } /** Update value and notify subscribers if changed */ set value(newValue: T) { if (Object.is(this._value, newValue)) return this._value = newValue this.subscribers.forEach((fn) => fn()) } /** Subscribe to value changes, returns cleanup function */ subscribe(fn: () => void): () => void { this.subscribers.add(fn) return () => this.subscribers.delete(fn) } /** Execute function while collecting Observable dependencies */ static collectDeps<T>(fn: () => T) { const prevDeps = Observable.currDeps const deps = new Set<Observable<any>>() Observable.currDeps = deps const value = fn() Observable.currDeps = prevDeps return { value, deps } } } /** Observable that automatically updates based on other Observable values */ class ComputedObservable<T> extends Observable<T> { private computeFn: () => T private cleanup: Array<() => void> = [] private isComputing = false private isDirty = true constructor(computeFn: () => T) { super(undefined as any) this.computeFn = computeFn this.recompute() } override get value(): T { if (this.isDirty && !this.isComputing) this.recompute() if (!this.isComputing && Observable.currDeps) Observable.currDeps.add(this) return this._value } private recompute(): void { if (this.isComputing) return this.isComputing = true try { this.cleanup.forEach((cleanup) => cleanup()) this.cleanup = [] const { value, deps } = Observable.collectDeps(this.computeFn) if (!Object.is(this._value, value)) { this._value = value this.subscribers.forEach((fn) => fn()) } deps.forEach((dep) => { this.cleanup.push( dep.subscribe(() => { if (!this.isComputing) { this.isDirty = true this.subscribers.forEach((fn) => fn()) } }) ) }) this.isDirty = false } finally { this.isComputing = false } } } type RawValue = number | string | boolean type StoreMethod<T> = (this: ComputedState<T>, ...args: any[]) => Partial<T> type ComputedProperty<T, R> = { get: (this: ComputedState<T>) => R } type StoreDefinition<T> = { [K in keyof T]: T[K] extends RawValue ? T[K] : T[K] extends (...args: any[]) => any ? StoreMethod<T> : T[K] extends { get: any } ? ComputedProperty<T, ReturnType<T[K]["get"]>> : never } type ComputedState<T> = { [K in keyof T]: T[K] extends RawValue ? T[K] : T[K] extends (...args: any[]) => any ? never : T[K] extends { get: (...args: any[]) => infer R } ? R : never } type Store<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? (...args: Parameters<T[K]>) => void : Observable<T[K]> } /** Creates a reactive store from a definition object */ export function createStore<T extends Record<string, any>>( definition: T ): Store<StoreDefinition<T>> { const store: Record<string, any> = {} const state = {} as ComputedState<T> const observables = new Map<keyof T, Observable<any>>() const computeds = new Map<keyof T, ComputedObservable<any>>() for (const key in definition) { const descriptor = Object.getOwnPropertyDescriptor(definition, key) if (!descriptor) continue if (descriptor.get) { const getter = descriptor.get const computed = new ComputedObservable(() => { const wrapper = {} as ComputedState<T> for (const k in store) if (store[k] && "value" in store[k]) Object.defineProperty(wrapper, k, { get: () => store[k].value, enumerable: true, }) return getter.call(wrapper) }) computeds.set(key as keyof T, computed) store[key] = computed Object.defineProperty(state, key, { get: () => computed.value, enumerable: true, }) } else if (typeof definition[key] !== "function") { const observable = new Observable(definition[key]) observables.set(key as keyof T, observable) store[key] = observable Object.defineProperty(state, key, { get: () => observable.value, set: (v) => (observable.value = v), enumerable: true, }) } } for (const key in definition) { const value = definition[key] if (typeof value !== "function" || computeds.has(key as keyof T)) continue store[key] = ((...args: any[]) => { const updates = value.apply(state, args) as Partial<T> if (!updates) return Object.entries(updates).forEach(([key, value]) => { if (key in state) (state[key as keyof T] as any) = value }) }) as T[keyof T] } return store as Store<StoreDefinition<T>> } /** React hook to subscribe to observable value changes */ function useStoreValue<T>(observable: Observable<T>): T { const [, forceUpdate] = React.useReducer((x) => x + 1, 0) React.useEffect(() => observable.subscribe(forceUpdate), [observable]) return observable.value } export default function MyComponent() { const a = useStoreValue(store.a) const b = useStoreValue(store.b) const sum = useStoreValue(store.sum) const sum2x = useStoreValue(store.sum2x) return ( <div> <h1>Brainstorming 11</h1> <div>a: {a}</div> <div>b: {b}</div> <div>sum: {sum}</div> <div>sum2x: {sum2x}</div> <button onClick={() => store.addA(5)}>Add to A</button> </div> ) } export const store = createStore({ a: 1, b: 2, addA(n: number) { return { a: this.a + n } }, get sum(): number { return this.a + this.b }, get sum2x(): number { return this.sum * 2 }, })