1.2.0 • Published 7 months ago
@qyu/atom-state-core v1.2.0
@qyu/atom-state-core
Definition and impelemntation of atomic state manager
Usage Example
const store = atomstore_new()
const width = atomvalue_new(() => {
    return 10
})
const height = atomvalue_new(() => {
    return 20
})
const size = atomvalue_new(store => {
    // store.reg will get existing value from store or register it
    return store.reg(width) * store.reg(height)
})
console.log(sotre.reg(size))Concept
- Create handlers - functions that accept store as parameter and do things
 - Calling it will store.reg() will return cached value or register it
 - All values in register are defined once and you should use signals for listen api and mutability
 
Atom Value
Will call cb and register return when called
const atomvalue = atomvalue_new(store => {
    return 10
})Atom State
Uses return as initial value for signal from @qyu/signal-core
const atomstate = atomstate_new(store => {
    return 10
})
store.reg(atomstate).input(50)
// 50
console.log(store.reg(atomstate).output())Atom Selector
Will return value based on store, but does not get cached. Useful for transforming state
const atomvalue = atomvalue_new(() => 10)
const atomselector = (store: AtomStore) => ({ 
    value: store.reg(atomvalue) 
})
// false
console.log(store.reg(atomselector) === store.reg(atomselector))Atom Action
Just a function that has AtomStore as a parameter
const atomvalue = atomvalue_new(() => 10)
const atomaction = (store: AtomStore) => {
    console.log(store.reg(atomvalue)) 
}
// 10
atomaction(store)Atom Family
Basically a cached map
const atomfamily = atomfamily_new({
    // value will be cached inside family
    get: (a: number, b: number) => atomvalue_new({ a, b }),
    // unique key that value will be cached with
    key: (a: number, b: number) => `${a} ${b}`
})
const family = store.reg(atomfamily)
family.reg(10, 51)Atom Selector Child
Utility to get child selector from family
const atomfamily = atomfamily_new({
    // value will be cached inside family
    get: (a: number, b: number) => atomvalue_new({ a, b }),
    // unique key that value will be cached with
    key: (a: number, b: number) => `${a} ${b}`
})
const selector = atomselector_new_child({
    params: [10, 15],
    family: atomfamily
})
// { a: 10, b: 15 }
console.log(store.reg(selector))Atom Remote
Represents Remote State
// reqphase is a special state that exists in three different variants: empty, pending, fulfilled
// it's pretty intuitive and this section has enough examples of usage with commets
const atomremote = atomremote_new<string>(store => reqphase_new_empty())
const remote = store.reg(atomremote)
const api_request = () => {
    let id: Timer
    return reqphase_new_pending<string>({
        promise: new Promise(resolve => {
            id = setTimeout(
                () => {
                    // may resolve with any kind of reqphase emptying state, initiating new request or fulfilling
                    resolve(reqphase_new_fulfilled("Hello World"))
                },
                500
            )
        }),
        abort: () => clearTimeout(id)
    })
}
remote.addsub(() => {
    console.log(
        // extract data from reqphase, use null on fallback, second paramter is set on default
        reqphase_data(remote.output(), () => null)
    )
})
// will pring null
remote.input(api_request())
// will abort previous request
// also will print null again
remote.input(api_request())
// in 0.5 seconds will pring "Hello World"Atom Selector Remote Data
Utility selector to get data from remotedata
const atomremote = atomremote_new<string>(() => reqphase_new_empty())
// second argument is fallback value, it's optional and () => null by default
const atomremote_data = atomselector_new_remotedata(atomremote, () => null)
const remote = store.reg(atomremote)
const remote_data = store.reg(atomremote_data)
remote_data.addsub(() => {
    console.log(remote_data.output())
})
remote.input(reqphase_new_empty())
remote.input(reqphase_new_fulfilled("Hello World"))
remote.input(reqphase_new_empty())Atom Loader
Connects when requested, disconnected when does not
Atom Loader Pure
Does not get any paramters Example with requesting user data
const atomstate_id = atomstate_new<number>(() => 0)
const atomremote = atomremote_new<string>(() => reqphase_new_empty())
// fake api request
const api_request_user = function (id: number, signal: AbortSignal): Promise<string> {
    return new Promise((resolve) => {
        let timer: Timer
        const interrupt = () => {
            resolve(reqphase_new_empty())
            clearTimeout(timer)
            signal.removeEventListener("abort", interrupt)
        }
        if (!signal.aborted) {
            signal.addEventListener("abort", interrupt)
            id = setTimeout(
                () => {
                    resolve(JSON.stringify({ name: "username", id }))
                },
                500
            )
        }
    })
}
const atomloader = atomloader_new_pure({
    // will start connection immediately when requested
    throttler: throttler_new_immediate(),
    // as alternative - use microtask throttler, it will schedule requests with Promise.resolve()
    // very usefull to prevent unnecessary requests when loader connection is unstable for some reason (eg. react effect hooks)
    // throttler: throttler_new_microtask(),
    connect: store => {
        const remote = store.reg(atomremote)
        const state_id = store.reg(atomstate_id)
        return signal_listen({
            target: state_id,
            config: {
                emit: true
            },
            listener: () => {
                const id = state_id.output()
                const abortcontroller = new AbortController()
                const promise = api_request_user(id, abortcontroller.signal)
                // adapt promise for expected format
                const promise_wrapped = promise.then(reqphase_new_fulfilled).catch(reqphase_new_empty)
                remote.input(reqphase_new_pending({
                    promise: promise_wrapped,
                    abort: () => abortcontroller.abort()
                }))
                return () => abortcontroller.abort()
            }
        })
    }
})
const remote = store.reg(atomremote)
const loader = store.reg(atomloader)
const state_id = store.reg(atomstate_id)
remote.addsub(() => {
    console.log(asc.reqphase_data(remote.output()))
})
// will start request for initial value of id
// calling request from multiple places will increase internal counter so you need to cancel all of them to interrupt loader
const cancel = loader.request()
// will interrupt previous one and start request for new input
state_id.input(50)
// change parameter after some time
setTimeout(() => {
    state_id.input(90)
    // cancel loader after some time, value stays in remote
    setTimeout(() => {
        cancel()
    }, 1000)
}, 1000)Atom Loader Concurrent
Similar to pure loader, but accepts parameter and calls connection with higher priority parameter, restarts every time it changes
const atomloader = atomloader_new_concurrent<[number, number]>({
    throttler: throttler_new_microtask(),
    comparator: (a, b) => {
        return a[0] + a[1] - b[0] - b[1]
    },
    connect: (a, b) => store => {
        console.log({ a, b })
        return () => {
            console.log("cleanup", { a, b })
        }
    }
})
const loader = store.reg(atomloader)
const cancel_low = loader.request(1, 2)
// will be on top
const cancel_top = loader.request(5, 7)
const cancel_mid = loader.request(3, 5)
// no printing anything yet because connection is throttled
setTimeout(() => {
    // does nothing
    cancel_mid()
    setTimeout(() => {
        // second one is on top so will reconnect
        cancel_top()
        setTimeout(() => {
            // clear connection completely
            cancel_low()
        }, 100)
    }, 100)
}, 100)