Skip to content

Instantly share code, notes, and snippets.

@beorn
Last active November 11, 2024 23:52
Show Gist options
  • Select an option

  • Save beorn/23c8bee75ac2acd4758ac0d3defe76e7 to your computer and use it in GitHub Desktop.

Select an option

Save beorn/23c8bee75ac2acd4758ac0d3defe76e7 to your computer and use it in GitHub Desktop.

Revisions

  1. beorn revised this gist Nov 11, 2024. 1 changed file with 9 additions and 5 deletions.
    14 changes: 9 additions & 5 deletions brainstorming11.tsx
    Original 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
    ? (...args: Parameters<T[K]>) => void
    ? 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
    store[key] = ((...args: any[]) => {

    // 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
    })
    }) as T[keyof T]
    }
    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={() => store.addA(5)}>Add to A</button>
    <button onClick={() => addA(5)}>Add to A</button>
    </div>
    )
    }
  2. beorn revised this gist Nov 11, 2024. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions brainstorming11.tsx
    Original 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 */
  3. beorn created this gist Nov 11, 2024.
    220 changes: 220 additions & 0 deletions brainstorming11.tsx
    Original 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
    },
    })