mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: use alien-signals
This commit is contained in:
parent
d7a1f62c3c
commit
861ee5acd7
76 changed files with 3112 additions and 1862 deletions
|
|
@ -67,7 +67,7 @@ local function client(options: ClientOptions): ClientSyncer
|
|||
for index = 1, select("#", ...) do
|
||||
local payload: SyncPayload = select(index, ...)
|
||||
|
||||
Charm.batch(function()
|
||||
Charm.batched(function()
|
||||
if payload.type == "init" then
|
||||
hydrate(payload.data)
|
||||
elseif not ignoreUnhydrated or hydrated then
|
||||
|
|
|
|||
|
|
@ -1,42 +1,54 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
local signals = require("./signals")
|
||||
local types = require("./types")
|
||||
|
||||
type Atom<T> = types.Atom<T>
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
|
||||
--[=[
|
||||
Creates a new atom with the given state.
|
||||
|
||||
@param state The initial state.
|
||||
@param options Optional configuration.
|
||||
@return A new atom.
|
||||
]=]
|
||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||
local equals = options and options.equals
|
||||
local signal = signals.signal
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function atom(...)
|
||||
if select("#", ...) == 0 then
|
||||
local index = store.capturing.index
|
||||
|
||||
if index > 0 then
|
||||
store.capturing.stack[index][atom] = true
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local nextState = store.peek(..., state)
|
||||
|
||||
if state ~= nextState and not (equals and equals(state, nextState)) then
|
||||
state = nextState
|
||||
store.notify(atom)
|
||||
end
|
||||
|
||||
return state
|
||||
local function atomOper<T>(source: (...T) -> T, equals: ((T, T) -> boolean)?, ...: ((T) -> T) | T): T
|
||||
if select("#", ...) == 0 then
|
||||
return source()
|
||||
end
|
||||
|
||||
store.listeners[atom] = setmetatable({}, { __mode = "v" })
|
||||
local transform = (...)
|
||||
local currentState = (source :: any).value
|
||||
local nextState: T
|
||||
|
||||
return atom
|
||||
-- If the transform is a function, call it with the current state and pause
|
||||
-- tracking to avoid adding signals called by the transform.
|
||||
if type(transform) == "function" then
|
||||
pauseTracking()
|
||||
local success, result = pcall(transform, currentState)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(result, 2)
|
||||
end
|
||||
|
||||
nextState = result
|
||||
else
|
||||
nextState = transform
|
||||
end
|
||||
|
||||
if equals and equals(currentState, nextState) then
|
||||
return nextState
|
||||
end
|
||||
|
||||
source(nextState)
|
||||
|
||||
return nextState
|
||||
end
|
||||
|
||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||
local source = signal(state)
|
||||
local equals = options and options.equals
|
||||
|
||||
return function(...)
|
||||
return atomOper(source, equals, ...)
|
||||
end
|
||||
end
|
||||
|
||||
return atom
|
||||
|
|
|
|||
16
packages/charm/src/batched.luau
Normal file
16
packages/charm/src/batched.luau
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local startBatch = signals.startBatch
|
||||
local endBatch = signals.endBatch
|
||||
|
||||
local function batched(callback: () -> ())
|
||||
startBatch()
|
||||
local success, result: any = pcall(callback)
|
||||
endBatch()
|
||||
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
|
||||
return batched
|
||||
|
|
@ -1,37 +1,13 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
type Selector<T> = types.Selector<T>
|
||||
local signals = require("./signals")
|
||||
|
||||
--[=[
|
||||
Creates a read-only atom that derives its state from one or more atoms.
|
||||
Used to avoid unnecessary recomputations if multiple listeners depend on
|
||||
the same atoms.
|
||||
local computedImpl = signals.computed
|
||||
|
||||
@param callback The function that produces the state.
|
||||
@param options Optional configuration.
|
||||
@return A new read-only atom.
|
||||
]=]
|
||||
local function computed<T>(callback: Selector<T>, options: AtomOptions<T>?): Selector<T>
|
||||
local dependencies, state = store.capture(callback)
|
||||
local computedAtom = atom(state, options)
|
||||
local computedRef = setmetatable({ current = computedAtom }, { __mode = "v" })
|
||||
local function computed<T>(selector: (previous: T?) -> T): () -> T
|
||||
local getter = computedImpl(selector)
|
||||
|
||||
local function listener()
|
||||
local computedAtom = computedRef.current
|
||||
|
||||
if computedAtom then
|
||||
store.disconnect(dependencies, listener)
|
||||
dependencies, state = store.capture(callback)
|
||||
store.connect(dependencies, listener, computedAtom)
|
||||
computedAtom(state)
|
||||
end
|
||||
return function()
|
||||
return getter()
|
||||
end
|
||||
|
||||
store.connect(dependencies, listener, computedAtom)
|
||||
|
||||
return computedAtom
|
||||
end
|
||||
|
||||
return computed
|
||||
|
|
|
|||
|
|
@ -1,53 +1,30 @@
|
|||
local store = require(script.Parent.store)
|
||||
local effectScope = require("./effectScope")
|
||||
local signals = require("./signals")
|
||||
|
||||
type Cleanup = () -> ()
|
||||
local effectImpl = signals.effect
|
||||
|
||||
--[=[
|
||||
Runs the given callback immediately and whenever any atom it depends on
|
||||
changes. Returns a cleanup function that unsubscribes the callback.
|
||||
|
||||
@param callback The function to run.
|
||||
@return A function that unsubscribes the callback.
|
||||
]=]
|
||||
local function effect(callback: (cleanup: Cleanup) -> Cleanup?): Cleanup
|
||||
local dependencies = {}
|
||||
local cleanup: Cleanup?
|
||||
local disconnected = false
|
||||
local disconnect
|
||||
local function effect(callback: (dispose: () -> ()) -> ...() -> ()?): () -> ()
|
||||
local disposed = false
|
||||
local dispose: () -> ()
|
||||
|
||||
local function listener()
|
||||
if cleanup then
|
||||
cleanup()
|
||||
end
|
||||
|
||||
store.disconnect(dependencies, listener)
|
||||
dependencies, cleanup = store.capture(callback, disconnect)
|
||||
|
||||
if not disconnected then
|
||||
store.connect(dependencies, listener)
|
||||
local function disposeSafe()
|
||||
disposed = true
|
||||
if dispose then
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
function disconnect()
|
||||
if disconnected then
|
||||
return
|
||||
end
|
||||
dispose = effectScope(function()
|
||||
effectImpl(function()
|
||||
return callback(disposeSafe)
|
||||
end)
|
||||
end)
|
||||
|
||||
disconnected = true
|
||||
store.disconnect(dependencies, listener)
|
||||
|
||||
if cleanup then
|
||||
cleanup()
|
||||
end
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
dependencies, cleanup = store.capture(callback, disconnect)
|
||||
|
||||
if not disconnected then
|
||||
store.connect(dependencies, listener)
|
||||
end
|
||||
|
||||
return disconnect
|
||||
return disposeSafe
|
||||
end
|
||||
|
||||
return effect :: (callback: ((cleanup: Cleanup) -> ()) | ((cleanup: Cleanup) -> Cleanup?)) -> Cleanup
|
||||
return effect
|
||||
|
|
|
|||
13
packages/charm/src/effectScope.luau
Normal file
13
packages/charm/src/effectScope.luau
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local effectScopeImpl = signals.effectScope
|
||||
|
||||
local function effectScope(callback: () -> ...() -> ()?): () -> ()
|
||||
local dispose = effectScopeImpl(callback)
|
||||
|
||||
return function()
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
return effectScope
|
||||
116
packages/charm/src/index.d.ts
vendored
116
packages/charm/src/index.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
export = Charm;
|
||||
export as namespace Charm;
|
||||
|
||||
type Cleanup = () => void;
|
||||
type Dispose = () => void;
|
||||
|
||||
declare namespace Charm {
|
||||
/**
|
||||
|
|
@ -11,7 +11,8 @@ declare namespace Charm {
|
|||
* @param state The next state or a function that produces the next state.
|
||||
* @returns The current state.
|
||||
*/
|
||||
interface Atom<State> extends Selector<State> {
|
||||
interface Atom<State> {
|
||||
(): State;
|
||||
(nextState: State | ((prevState: State) => State)): State;
|
||||
}
|
||||
|
||||
|
|
@ -23,19 +24,10 @@ declare namespace Charm {
|
|||
*/
|
||||
type Selector<State> = () => State;
|
||||
|
||||
/**
|
||||
* A function that depends on one or more atoms and produces a value. Can be
|
||||
* used to derive state from atoms.
|
||||
*
|
||||
* @deprecated Use `Selector<T>` instead.
|
||||
* @returns The current state.
|
||||
*/
|
||||
type Molecule<State> = Selector<State>;
|
||||
|
||||
/**
|
||||
* Infers the type of the state produced by the given function.
|
||||
*/
|
||||
type StateOf<T> = T extends Selector<infer State> ? State : never;
|
||||
type StateOf<T> = T extends () => infer State ? State : never;
|
||||
|
||||
interface AtomOptions<State> {
|
||||
/**
|
||||
|
|
@ -63,21 +55,34 @@ declare namespace Charm {
|
|||
* the same atoms.
|
||||
*
|
||||
* @param callback The function that produces the state.
|
||||
* @param options Optional configuration.
|
||||
* @returns A new read-only atom.
|
||||
*/
|
||||
function computed<State>(callback: Selector<State>, options?: AtomOptions<State>): Selector<State>;
|
||||
function computed<State>(callback: (prevResult?: State) => State): () => State;
|
||||
|
||||
/**
|
||||
* Subscribes to changes in the given atom or selector. The callback is
|
||||
* called with the current state and the previous state immediately after a
|
||||
* change occurs.
|
||||
* called with the current state and the previous state only after a change
|
||||
* occurs.
|
||||
*
|
||||
* @param callback The atom or selector to subscribe to.
|
||||
* @param listener The function to call when the state changes.
|
||||
* @returns A function that unsubscribes the callback.
|
||||
*/
|
||||
function subscribe<State>(callback: Selector<State>, listener: (state: State, prev: State) => void): Cleanup;
|
||||
function subscribe<State>(callback: () => State, listener: (state: State, prevState: State) => void): Dispose;
|
||||
|
||||
/**
|
||||
* Subscribes to changes in the given atom or selector. The callback is
|
||||
* called with the current state and the previous state immediately after
|
||||
* subscribing, and again when a change occurs.
|
||||
*
|
||||
* @param callback The atom or selector to subscribe to.
|
||||
* @param listener The function to call when the state changes.
|
||||
* @returns A function that unsubscribes the callback.
|
||||
*/
|
||||
function listen<State>(
|
||||
callback: () => State,
|
||||
listener: (state: State, prevState: State, dispose: Dispose) => void,
|
||||
): Dispose;
|
||||
|
||||
/**
|
||||
* Runs the given callback immediately and whenever any atom it depends on
|
||||
|
|
@ -86,25 +91,7 @@ declare namespace Charm {
|
|||
* @param callback The function to run.
|
||||
* @returns A function that unsubscribes the callback.
|
||||
*/
|
||||
function effect(callback: () => Cleanup | void): Cleanup;
|
||||
|
||||
/**
|
||||
* Returns the result of the function without subscribing to changes. If a
|
||||
* non-function value is provided, it is returned as is.
|
||||
*
|
||||
* @param callback The atom or selector to get the state of.
|
||||
* @param args Arguments to pass to the function.
|
||||
* @returns The current state.
|
||||
*/
|
||||
function peek<State, Args extends unknown[]>(callback: State | ((...args: Args) => State), ...args: Args): State;
|
||||
|
||||
/**
|
||||
* Returns whether the given value is an atom.
|
||||
*
|
||||
* @param value The value to check.
|
||||
* @returns `true` if the value is an atom, otherwise `false`.
|
||||
*/
|
||||
function isAtom(value: unknown): value is Atom<any>;
|
||||
function effect(callback: (dispose: Dispose) => Dispose | void): Dispose;
|
||||
|
||||
/**
|
||||
* Runs the given function and schedules listeners to be notified only once
|
||||
|
|
@ -112,24 +99,16 @@ declare namespace Charm {
|
|||
*
|
||||
* @param callback The function to run.
|
||||
*/
|
||||
function batch(callback: () => void): void;
|
||||
function batched(callback: () => void): void;
|
||||
|
||||
/**
|
||||
* Captures all atoms that are read during the function call and returns them
|
||||
* along with the result of the function. Useful for tracking dependencies.
|
||||
* Returns the result of the function without subscribing to changes.
|
||||
*
|
||||
* @param callback The function to run.
|
||||
* @returns A tuple containing the captured atoms and the result of the function.
|
||||
* @param callback The atom or selector to get the state of.
|
||||
* @param args Arguments to pass to the function.
|
||||
* @returns The current state.
|
||||
*/
|
||||
function capture<State>(callback: Selector<State>): LuaTuple<[dependencies: Set<Atom<unknown>>, state: State]>;
|
||||
|
||||
/**
|
||||
* Notifies all subscribers of the given atom that the state has changed.
|
||||
* Mainly for running effects, since subscriptions check for equality.
|
||||
*
|
||||
* @param atom The atom to notify.
|
||||
*/
|
||||
function notify<State>(atom: Atom<State>): void;
|
||||
function untracked<State, Args extends unknown[]>(callback: (...args: Args) => State, ...args: Args): State;
|
||||
|
||||
/**
|
||||
* Creates an instance of `factory` for each item in the atom's state, and
|
||||
|
|
@ -141,47 +120,46 @@ declare namespace Charm {
|
|||
* @returns A function that unsubscribes all instances.
|
||||
*/
|
||||
function observe<Item>(
|
||||
callback: Selector<readonly Item[]>,
|
||||
factory: (item: Item, index: number) => Cleanup | void,
|
||||
): Cleanup;
|
||||
callback: () => readonly Item[],
|
||||
factory: (item: Item, index: number) => Dispose | void,
|
||||
): Dispose;
|
||||
|
||||
function observe<Key, Item>(
|
||||
callback: Selector<Map<Key, Item> | ReadonlyMap<Key, Item>>,
|
||||
factory: (item: Item, key: Key) => Cleanup | void,
|
||||
): Cleanup;
|
||||
callback: () => Map<Key, Item> | ReadonlyMap<Key, Item>,
|
||||
factory: (item: Item, key: Key) => Dispose | void,
|
||||
): Dispose;
|
||||
|
||||
function observe<Key extends string | number | symbol, Item>(
|
||||
callback: Selector<{ readonly [P in Key]: Item }>,
|
||||
factory: (item: Item, key: Key) => Cleanup | void,
|
||||
): Cleanup;
|
||||
callback: () => { readonly [P in Key]: Item },
|
||||
factory: (item: Item, key: Key) => Dispose | void,
|
||||
): Dispose;
|
||||
|
||||
/**
|
||||
* Maps each entry in the atom's state to a new key-value pair. If the `mapper`
|
||||
* function returns `undefined`, the entry is omitted from the resulting map.
|
||||
* When the atom changes, the `mapper` is called for each entry in the state
|
||||
* to compute the new state.
|
||||
* When the atom changes, the `mapper` only runs for the changed entries.
|
||||
*
|
||||
* @param callback The atom or selector to map.
|
||||
* @param mapper The function that maps each entry.
|
||||
* @returns A new atom with the mapped state.
|
||||
*/
|
||||
function mapped<V0, K1, V1>(
|
||||
callback: Selector<readonly V0[]>,
|
||||
callback: () => readonly V0[],
|
||||
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
||||
): Selector<ReadonlyMap<K1, V1>>;
|
||||
): () => ReadonlyMap<K1, V1>;
|
||||
|
||||
function mapped<V0, V1>(
|
||||
callback: Selector<readonly V0[]>,
|
||||
callback: () => readonly V0[],
|
||||
mapper: (value: V0, index: number) => V1,
|
||||
): Selector<readonly V1[]>;
|
||||
): () => readonly V1[];
|
||||
|
||||
function mapped<K0, V0, K1 = K0, V1 = V0>(
|
||||
callback: Selector<Map<K0, V0> | ReadonlyMap<K0, V0>>,
|
||||
callback: () => Map<K0, V0> | ReadonlyMap<K0, V0>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
||||
): Selector<ReadonlyMap<K1, V1>>;
|
||||
): () => ReadonlyMap<K1, V1>;
|
||||
|
||||
function mapped<K0 extends string | number | symbol, V0, K1 = K0, V1 = V0>(
|
||||
callback: Selector<{ readonly [P in K0]: V0 }>,
|
||||
callback: () => { readonly [P in K0]: V0 },
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
||||
): Selector<ReadonlyMap<K1, V1>>;
|
||||
): () => ReadonlyMap<K1, V1>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,7 @@
|
|||
local atom = require(script.atom)
|
||||
local computed = require(script.computed)
|
||||
local effect = require(script.effect)
|
||||
local mapped = require(script.mapped)
|
||||
local observe = require(script.observe)
|
||||
local store = require(script.store)
|
||||
local subscribe = require(script.subscribe)
|
||||
local types = require(script.types)
|
||||
local lib = require(script.lib)
|
||||
|
||||
export type Atom<State> = types.Atom<State>
|
||||
export type Selector<State> = types.Selector<State>
|
||||
export type Molecule<State> = types.Selector<State>
|
||||
export type SyncPayload = types.SyncPayload
|
||||
export type Atom<T = any> = lib.Atom<T>
|
||||
export type AtomOptions<T = any> = lib.AtomOptions<T>
|
||||
export type Selector<T = any> = lib.Selector<T>
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
batch = store.batch,
|
||||
capture = store.capture,
|
||||
isAtom = store.isAtom,
|
||||
notify = store.notify,
|
||||
peek = store.peek,
|
||||
}
|
||||
return lib
|
||||
|
|
|
|||
28
packages/charm/src/lib.luau
Normal file
28
packages/charm/src/lib.luau
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
local atom = require("./atom")
|
||||
local batched = require("./batched")
|
||||
local computed = require("./computed")
|
||||
local effect = require("./effect")
|
||||
local effectScope = require("./effectScope")
|
||||
local listen = require("./listen")
|
||||
local mapped = require("./mapped")
|
||||
local observe = require("./observe")
|
||||
local subscribe = require("./subscribe")
|
||||
local types = require("./types")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
export type Atom<T = any> = types.Atom<T>
|
||||
export type AtomOptions<T = any> = types.AtomOptions<T>
|
||||
export type Selector<T = any> = types.Selector<T>
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
batched = batched,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
listen = listen,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
}
|
||||
40
packages/charm/src/listen.luau
Normal file
40
packages/charm/src/listen.luau
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
local effectScope = require("./effectScope")
|
||||
local signals = require("./signals")
|
||||
local spawn = require("./spawn")
|
||||
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local disposed = false
|
||||
local current: T?
|
||||
local dispose: () -> ()
|
||||
|
||||
local function disposeSafe()
|
||||
disposed = true
|
||||
if dispose then
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
dispose = effectScope(function()
|
||||
effect(function()
|
||||
local value = selector()
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous, disposeSafe)
|
||||
resumeTracking()
|
||||
end)
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return disposeSafe
|
||||
end
|
||||
|
||||
return listen
|
||||
|
|
@ -1,71 +1,127 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
local store = require(script.Parent.store)
|
||||
local subscribe = require(script.Parent.subscribe)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Selector<T> = types.Selector<T>
|
||||
local computed = require("./computed")
|
||||
local signals = require("./signals")
|
||||
|
||||
type Map =
|
||||
(<K0, V0, K1, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1)) -> Selector<{ [K1]: V1 }>)
|
||||
& (<K0, V0, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Selector<{ [K0]: V1 }>)
|
||||
& (<K0, V0, K1, V1>(fn: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Selector<{ [K1]: V1 }>)
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
--[=[
|
||||
Maps each entry in the atom's state to a new key-value pair. If the `mapper`
|
||||
function returns `undefined`, the entry is omitted from the resulting map.
|
||||
When the atom changes, the `mapper` is called for each entry in the state
|
||||
to compute the new state.
|
||||
|
||||
@param callback The atom or selector to map.
|
||||
@param mapper The function that maps each entry.
|
||||
@return A new atom with the mapped state.
|
||||
]=]
|
||||
local function mapped<K0, V0, K1, V1>(callback: Selector<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Selector<{ [K1]: V1 }>
|
||||
local mappedAtom = atom({})
|
||||
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
||||
local prevMappedItems: { [K1]: V1 } = {}
|
||||
local unsubscribe
|
||||
type MappedKey<VI, KO, VO> = {
|
||||
key: KO,
|
||||
value: VO,
|
||||
}
|
||||
|
||||
local function listener(items: { [K0]: V0 })
|
||||
local mappedAtom = mappedAtomRef.current
|
||||
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (item: VI, key: KI) -> (VO, KO?)): () -> { [KO]: VO }
|
||||
local mappedKeys: { [KI]: MappedKey<VI, KO, VO> } = {}
|
||||
local previousItems: { [KI]: VI }?
|
||||
|
||||
if not mappedAtom then
|
||||
return unsubscribe()
|
||||
local function getTransform(value: VI, key: KI): (VO, KO?)
|
||||
pauseTracking()
|
||||
local success, valueOut, keyOut = pcall(transform, value, key)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(`observe() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
local mappedItems = table.clone(mappedAtom())
|
||||
local mappedKeys = {}
|
||||
|
||||
-- TODO: Only call mapper if the item has changed.
|
||||
for key, item in next, items do
|
||||
local newItem, newKey = mapper(item, key)
|
||||
if newKey == nil then
|
||||
newKey = key :: any
|
||||
end
|
||||
if mappedItems[newKey :: K1] ~= newItem then
|
||||
mappedItems[newKey :: K1] = newItem :: V1
|
||||
else
|
||||
mappedKeys[newKey] = key
|
||||
end
|
||||
end
|
||||
|
||||
for key in next, prevMappedItems do
|
||||
if mappedKeys[key] == nil and mappedItems[key] == prevMappedItems[key] then
|
||||
mappedItems[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
prevMappedItems = mappedItems
|
||||
mappedAtom(mappedItems)
|
||||
return valueOut, keyOut
|
||||
end
|
||||
|
||||
unsubscribe = subscribe(callback, listener)
|
||||
return computed(function(previousMappedItems)
|
||||
local currentItems = selector()
|
||||
|
||||
store.peek(function(): ()
|
||||
listener(callback())
|
||||
-- If this is the initial run, create a map of the items and store the
|
||||
-- updated keys and values.
|
||||
if not previousItems or not previousMappedItems then
|
||||
local mappedItems: { [KO]: VO } = {}
|
||||
previousItems = currentItems
|
||||
|
||||
for key, value in currentItems do
|
||||
local valueOut, keyOut = getTransform(value, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
end
|
||||
|
||||
return mappedItems
|
||||
end
|
||||
|
||||
local mappedItems = table.clone(previousMappedItems)
|
||||
|
||||
-- Update and remove mapped keys if the source values changed
|
||||
for key, oldValue in previousItems do
|
||||
local currentValue = currentItems[key]
|
||||
local mappedKey = mappedKeys[key]
|
||||
|
||||
-- Remove the key if the source value changed from non-nil to nil
|
||||
if currentValue == nil then
|
||||
mappedKeys[key] = nil
|
||||
if mappedKey then
|
||||
mappedItems[mappedKey.key] = nil
|
||||
end
|
||||
continue
|
||||
end
|
||||
|
||||
-- Do not call transform if the value has not changed
|
||||
if currentValue == oldValue then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = getTransform(currentValue, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
-- If there is no previous mappedKey, then this item was filtered
|
||||
-- out to optimize memory. We need to check if the value is nil to
|
||||
-- determine if we should add it back to the list.
|
||||
if not mappedKey then
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
continue
|
||||
end
|
||||
|
||||
-- If the mapped value is nil, then delete mappedKey to save memory.
|
||||
-- This is treated the same as setting mappedKey.value to nil.
|
||||
if valueOut == nil then
|
||||
mappedKeys[key] = nil
|
||||
mappedItems[mappedKey.key] = nil
|
||||
continue
|
||||
end
|
||||
|
||||
-- If the key changed since the last transform call, remove
|
||||
-- the old key and update mappedKey.
|
||||
if keyOut ~= mappedKey.key then
|
||||
mappedItems[mappedKey.key] = nil
|
||||
mappedKey.key = keyOut
|
||||
end
|
||||
|
||||
mappedKey.value = valueOut
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
|
||||
-- Add new keys if the value was not present in the previous state
|
||||
for key, currentValue in currentItems do
|
||||
local oldValue = previousItems[key]
|
||||
|
||||
if oldValue ~= nil then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = getTransform(currentValue, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
end
|
||||
|
||||
previousItems = currentItems
|
||||
|
||||
return mappedItems
|
||||
end)
|
||||
|
||||
return mappedAtom
|
||||
end
|
||||
|
||||
return mapped :: Map
|
||||
return mapped
|
||||
|
|
|
|||
|
|
@ -1,49 +1,37 @@
|
|||
local store = require(script.Parent.store)
|
||||
local subscribe = require(script.Parent.subscribe)
|
||||
local types = require(script.Parent.types)
|
||||
type Selector<T> = types.Selector<T>
|
||||
local listen = require("./listen")
|
||||
|
||||
local function noop() end
|
||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...() -> ()?): () -> ()
|
||||
local cleanups: { [K]: () -> ()? } = {}
|
||||
|
||||
--[=[
|
||||
Creates an instance of `factory` for each item in the atom's state, and
|
||||
cleans up the instance when the item is removed. Returns a cleanup function
|
||||
that unsubscribes all instances.
|
||||
|
||||
@param callback The atom or selector to observe.
|
||||
@param factory The function that tracks the lifecycle of each item.
|
||||
@return A function that unsubscribes all instances.
|
||||
]=]
|
||||
local function observe<K, V>(callback: Selector<{ [K]: V }>, factory: (value: V, key: K) -> (() -> ())?): () -> ()
|
||||
local connections: { [K]: () -> () } = {}
|
||||
local unsub = listen(selector, function(currentItems, previousItems)
|
||||
if not previousItems then
|
||||
for key, value in currentItems do
|
||||
cleanups[key] = observer(value, key)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local function listener(state: { [K]: V })
|
||||
for key, disconnect in next, connections do
|
||||
if state[key] == nil then
|
||||
connections[key] = nil
|
||||
disconnect()
|
||||
for key in previousItems do
|
||||
local cleanup = cleanups[key]
|
||||
if currentItems[key] == nil and cleanup then
|
||||
cleanups[key] = nil
|
||||
cleanup()
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in next, state do
|
||||
if not connections[key] then
|
||||
connections[key] = factory(value, key) or noop
|
||||
for key, value in currentItems do
|
||||
if previousItems[key] == nil then
|
||||
cleanups[key] = observer(value, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local unsubscribe = subscribe(callback, listener)
|
||||
|
||||
store.peek(function(): ()
|
||||
listener(callback())
|
||||
end)
|
||||
|
||||
return function()
|
||||
unsubscribe()
|
||||
for _, disconnect in next, connections do
|
||||
disconnect()
|
||||
unsub()
|
||||
for _, cleanup in cleanups do
|
||||
(cleanup :: () -> ())()
|
||||
end
|
||||
table.clear(connections)
|
||||
table.clear(cleanups)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
400
packages/charm/src/signals.luau
Normal file
400
packages/charm/src/signals.luau
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
--[=[
|
||||
alien-signals 2.0.1 implementation with effect cleanups, adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
|
||||
local system = require("./system")
|
||||
|
||||
export type ReactiveFlags = system.ReactiveFlags
|
||||
export type ReactiveNode = system.ReactiveNode
|
||||
export type Link = system.Link
|
||||
|
||||
local ReactiveFlags = system.ReactiveFlags
|
||||
local createReactiveSystem = system.createReactiveSystem
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local QUEUED = 0b1000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
cleanup: (() -> ())?,
|
||||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (() -> ())?,
|
||||
cleanup: (() -> ())?,
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
value: T?,
|
||||
getter: (previousValue: T?) -> T,
|
||||
}
|
||||
|
||||
type Signal<T = any> = ReactiveNode & {
|
||||
previousValue: T,
|
||||
value: T,
|
||||
}
|
||||
|
||||
local pauseStack: { ReactiveNode? } = {}
|
||||
local pauseStackCount = 0
|
||||
local queuedEffects: { Effect | EffectScope } = {}
|
||||
|
||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||
local propagate: (link: Link) -> ()
|
||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
||||
local endTracking: (sub: ReactiveNode) -> ()
|
||||
local startTracking: (sub: ReactiveNode) -> ()
|
||||
local shallowPropagate: (link: Link) -> ()
|
||||
|
||||
local batchDepth = 0
|
||||
|
||||
local notifyIndex = 0
|
||||
local queuedEffectsLength = 0
|
||||
local activeSub: ReactiveNode?
|
||||
local activeScope: EffectScope?
|
||||
|
||||
local function getCurrentSub(): ReactiveNode?
|
||||
return activeSub
|
||||
end
|
||||
|
||||
local function setCurrentSub(sub: ReactiveNode?)
|
||||
local prevSub = activeSub
|
||||
activeSub = sub
|
||||
return prevSub
|
||||
end
|
||||
|
||||
local function getCurrentScope(): EffectScope?
|
||||
return activeScope
|
||||
end
|
||||
|
||||
local function setCurrentScope(scope: EffectScope?)
|
||||
local prevScope = activeScope
|
||||
activeScope = scope
|
||||
return prevScope
|
||||
end
|
||||
|
||||
local function pauseTracking()
|
||||
pauseStackCount += 1
|
||||
pauseStack[pauseStackCount] = activeSub
|
||||
activeSub = nil
|
||||
end
|
||||
|
||||
local function resumeTracking()
|
||||
activeSub = pauseStack[pauseStackCount]
|
||||
pauseStack[pauseStackCount] = nil
|
||||
pauseStackCount -= 1
|
||||
end
|
||||
|
||||
local function cleanup(e: ReactiveNode)
|
||||
local e = e :: EffectScope
|
||||
local cleanup = e.cleanup
|
||||
if cleanup then
|
||||
e.cleanup = nil
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(cleanup)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function run(e: EffectScope, flags: ReactiveFlags): ()
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
||||
cleanup(e)
|
||||
local prev = setCurrentSub(e)
|
||||
startTracking(e)
|
||||
local success, result = pcall((e :: Effect).fn)
|
||||
setCurrentSub(prev)
|
||||
endTracking(e)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
local link = e.deps
|
||||
while link do
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
if bit32.band(depFlags, QUEUED) ~= 0 then
|
||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||
dep.flags = depFlags
|
||||
run(dep :: EffectScope, depFlags)
|
||||
end
|
||||
link = link.nextDep
|
||||
end
|
||||
end
|
||||
|
||||
local function flush(): ()
|
||||
while notifyIndex < queuedEffectsLength do
|
||||
notifyIndex += 1
|
||||
local effect = queuedEffects[notifyIndex] :: EffectScope
|
||||
queuedEffects[notifyIndex] = nil
|
||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
||||
run(effect, effect.flags)
|
||||
end
|
||||
notifyIndex = 0
|
||||
queuedEffectsLength = 0
|
||||
end
|
||||
|
||||
local function getBatchDepth(): number
|
||||
return batchDepth
|
||||
end
|
||||
|
||||
local function startBatch()
|
||||
batchDepth += 1
|
||||
end
|
||||
|
||||
local function endBatch()
|
||||
batchDepth -= 1
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
|
||||
local function updateComputed(c: Computed): boolean
|
||||
local prevSub = setCurrentSub(c)
|
||||
startTracking(c)
|
||||
local oldValue = c.value
|
||||
local success, result = pcall(c.getter, oldValue)
|
||||
setCurrentSub(prevSub)
|
||||
endTracking(c)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
if oldValue ~= result then
|
||||
c.value = result
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function updateSignal(s: Signal, value: any): boolean
|
||||
s.flags = MUTABLE
|
||||
if s.previousValue ~= value then
|
||||
s.previousValue = value
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function signalOper<T>(this: Signal<T>, ...: T): T?
|
||||
if select("#", ...) > 0 then
|
||||
local newValue = (...)
|
||||
if this.value ~= newValue then
|
||||
this.value = newValue
|
||||
this.flags = bit32.bor(MUTABLE, DIRTY)
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
propagate(subs)
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
else
|
||||
local value = this.value
|
||||
if bit32.band(this.flags, DIRTY) ~= 0 then
|
||||
if updateSignal(this, value) then
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
local function computedOper<T>(this: Computed<T>): T
|
||||
local flags = this.flags
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this)) then
|
||||
if updateComputed(this) then
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
elseif activeScope then
|
||||
link(this, activeScope)
|
||||
end
|
||||
return this.value :: T
|
||||
end
|
||||
|
||||
local function effectOper(this: EffectScope): ()
|
||||
local dep = this.deps
|
||||
while dep do
|
||||
dep = unlink(dep, this)
|
||||
end
|
||||
local sub = this.subs
|
||||
while sub do
|
||||
unlink(sub)
|
||||
sub = this.subs
|
||||
end
|
||||
this.flags = NONE
|
||||
cleanup(this)
|
||||
end
|
||||
|
||||
local signalMetatable = { __call = signalOper }
|
||||
local computedMetatable = { __call = computedOper }
|
||||
local effectMetatable = { __call = effectOper }
|
||||
|
||||
local function signal<T>(initialValue: T): ((T) -> ()) & (() -> T)
|
||||
local this: Signal<T> = {
|
||||
previousValue = initialValue,
|
||||
value = initialValue,
|
||||
flags = MUTABLE,
|
||||
}
|
||||
return setmetatable(this, signalMetatable) :: any
|
||||
end
|
||||
|
||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||
local this: Computed<T> = {
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
getter = getter,
|
||||
}
|
||||
return setmetatable(this, computedMetatable) :: any
|
||||
end
|
||||
|
||||
local function effect(fn: () -> ...() -> ()?): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
}
|
||||
if activeSub then
|
||||
link(e, activeSub)
|
||||
elseif activeScope then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result = pcall(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
end
|
||||
|
||||
local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
||||
local e: EffectScope = {
|
||||
flags = NONE,
|
||||
}
|
||||
if activeScope then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result = pcall(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
end
|
||||
|
||||
local function notify(e: ReactiveNode)
|
||||
local flags = e.flags
|
||||
if bit32.band(flags, QUEUED) == 0 then
|
||||
e.flags = bit32.bor(flags, QUEUED)
|
||||
local subs = e.subs
|
||||
if subs then
|
||||
notify(subs.sub :: Effect | EffectScope)
|
||||
else
|
||||
queuedEffectsLength += 1
|
||||
queuedEffects[queuedEffectsLength] = e :: Effect | EffectScope
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function update(signal: ReactiveNode): boolean
|
||||
if (signal :: Computed).getter then
|
||||
return updateComputed(signal :: Computed)
|
||||
else
|
||||
return updateSignal(signal :: Signal, (signal :: Signal).value)
|
||||
end
|
||||
end
|
||||
|
||||
local function unwatched(signal: ReactiveNode)
|
||||
local toRemove = signal.deps
|
||||
if toRemove then
|
||||
repeat
|
||||
toRemove = unlink(toRemove, signal)
|
||||
until not toRemove
|
||||
signal.flags = bit32.bor(signal.flags, DIRTY)
|
||||
end
|
||||
cleanup(signal)
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = update,
|
||||
notify = notify,
|
||||
unwatched = unwatched,
|
||||
})
|
||||
|
||||
link = reactiveSystem.link
|
||||
unlink = reactiveSystem.unlink
|
||||
propagate = reactiveSystem.propagate
|
||||
checkDirty = reactiveSystem.checkDirty
|
||||
endTracking = reactiveSystem.endTracking
|
||||
startTracking = reactiveSystem.startTracking
|
||||
shallowPropagate = reactiveSystem.shallowPropagate
|
||||
|
||||
return {
|
||||
ReactiveFlags = ReactiveFlags,
|
||||
createReactiveSystem = createReactiveSystem,
|
||||
getCurrentSub = getCurrentSub,
|
||||
setCurrentSub = setCurrentSub,
|
||||
getCurrentScope = getCurrentScope,
|
||||
setCurrentScope = setCurrentScope,
|
||||
getBatchDepth = getBatchDepth,
|
||||
startBatch = startBatch,
|
||||
endBatch = endBatch,
|
||||
pauseTracking = pauseTracking,
|
||||
resumeTracking = resumeTracking,
|
||||
signal = signal,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
}
|
||||
36
packages/charm/src/spawn.luau
Normal file
36
packages/charm/src/spawn.luau
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
local freeThread: thread?
|
||||
|
||||
local function run<T...>(thread: thread, fn: (T...) -> (), ...: T...): boolean
|
||||
fn(...)
|
||||
if freeThread then
|
||||
return true
|
||||
else
|
||||
freeThread = thread
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function runner()
|
||||
local quit = false
|
||||
repeat
|
||||
quit = run(coroutine.yield())
|
||||
until quit
|
||||
end
|
||||
|
||||
local function spawn<T...>(callback: (T...) -> (), ...: T...)
|
||||
local thread
|
||||
|
||||
if freeThread then
|
||||
thread = freeThread
|
||||
freeThread = nil
|
||||
elseif task then
|
||||
thread = task.spawn(runner)
|
||||
else
|
||||
thread = coroutine.create(runner)
|
||||
coroutine.resume(thread)
|
||||
end
|
||||
|
||||
coroutine.resume(thread, thread, callback, ...)
|
||||
end
|
||||
|
||||
return spawn
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Set<T> = types.Set<T>
|
||||
type WeakMap<K, V> = types.WeakMap<K, V>
|
||||
|
||||
local __DEV__ = _G.__DEV__
|
||||
|
||||
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local batched: Set<() -> ()> = {}
|
||||
local batching = false
|
||||
|
||||
local capturing = {
|
||||
stack = {} :: { Set<Atom<any>> },
|
||||
index = 0,
|
||||
}
|
||||
|
||||
--[=[
|
||||
Calls the given function and returns the result. If the function yields or
|
||||
throws an error, the thread is closed and an error is thrown. Regardless of
|
||||
the outcome, the `finally` function is called to clean up any resources.
|
||||
|
||||
@param callback The function to run.
|
||||
@param finally Cleanup logic to run before error handling.
|
||||
@param ... Arguments to pass to the callback.
|
||||
@return The result of the callback.
|
||||
]=]
|
||||
local function try<T, U...>(callback: (U...) -> T | any, finally: (() -> ())?, ...: U...): T
|
||||
if __DEV__ then
|
||||
local thread = coroutine.create(callback)
|
||||
local success, result = coroutine.resume(thread, ...)
|
||||
|
||||
if finally then
|
||||
finally()
|
||||
end
|
||||
|
||||
if coroutine.status(thread) == "suspended" then
|
||||
local source, line, name = debug.info(callback, "sln")
|
||||
|
||||
coroutine.close(thread)
|
||||
|
||||
error(
|
||||
"Yielding is not allowed in atom functions. Consider wrapping this code in a Promise or task.defer instead."
|
||||
.. `\nFunction defined at: {source}:{line}`
|
||||
.. if name == "" then "" else ` function {name}`
|
||||
)
|
||||
elseif not success then
|
||||
local source, line, name = debug.info(callback, "sln")
|
||||
|
||||
error(
|
||||
"An error occurred while running an atom function"
|
||||
.. `\nFunction defined at: {source}:{line}`
|
||||
.. (if name == "" then "" else ` function {name}`)
|
||||
.. `\nError: {result}`
|
||||
)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
if not finally then
|
||||
return callback(...)
|
||||
end
|
||||
|
||||
local success, result = pcall(callback, ...)
|
||||
|
||||
finally()
|
||||
assert(success, result)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[=[
|
||||
Returns whether the given value is an atom.
|
||||
|
||||
@param value The value to check.
|
||||
@return `true` if the value is an atom, otherwise `false`.
|
||||
]=]
|
||||
local function isAtom(value: any): boolean
|
||||
return not not (value and listeners[value])
|
||||
end
|
||||
|
||||
--[=[
|
||||
Notifies all subscribers of the given atom that the state has changed.
|
||||
|
||||
@param atom The atom to notify.
|
||||
]=]
|
||||
local function notify(atom: Atom<any>)
|
||||
if batching then
|
||||
for listener in next, listeners[atom] do
|
||||
batched[listener] = true
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
for listener in next, table.clone(listeners[atom]) do
|
||||
try(listener)
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
Returns the result of the function without subscribing to changes. If a
|
||||
non-function value is provided, it is returned as is.
|
||||
|
||||
@param callback The atom or selector to get the state of.
|
||||
@param args Arguments to pass to the function.
|
||||
@return The current state.
|
||||
]=]
|
||||
local function peek<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
|
||||
if type(callback) ~= "function" then
|
||||
return callback
|
||||
end
|
||||
|
||||
if capturing.index == 0 then
|
||||
return callback(...)
|
||||
end
|
||||
|
||||
capturing.index += 1
|
||||
capturing.stack[capturing.index] = {}
|
||||
|
||||
local result = try(callback, function()
|
||||
capturing.stack[capturing.index] = nil
|
||||
capturing.index -= 1
|
||||
end, ...)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[=[
|
||||
Captures all atoms that are read during the function call and returns them
|
||||
along with the result of the function. Useful for tracking dependencies.
|
||||
|
||||
@param callback The function to run.
|
||||
@return A tuple containing the captured atoms and the result of the function.
|
||||
]=]
|
||||
local function capture<T, U...>(callback: (U...) -> T, ...: U...): (Set<Atom<any>>, T)
|
||||
-- If the callback is an atom, return it immediately
|
||||
if listeners[callback :: any] then
|
||||
return { [callback :: any] = true }, peek(callback)
|
||||
end
|
||||
|
||||
local dependencies: Set<Atom<any>> = {}
|
||||
|
||||
capturing.index += 1
|
||||
capturing.stack[capturing.index] = dependencies
|
||||
|
||||
local result = try(callback, function()
|
||||
capturing.stack[capturing.index] = nil
|
||||
capturing.index -= 1
|
||||
end, ...)
|
||||
|
||||
return dependencies, result
|
||||
end
|
||||
|
||||
--[=[
|
||||
Runs the given function and schedules listeners to be notified only once
|
||||
after the function has completed. Useful for batching multiple changes.
|
||||
|
||||
@param callback The function to run.
|
||||
]=]
|
||||
local function batch(callback: () -> ())
|
||||
if batching then
|
||||
return callback()
|
||||
end
|
||||
|
||||
batching = true
|
||||
|
||||
try(callback, function()
|
||||
batching = false
|
||||
end)
|
||||
|
||||
for listener in next, batched do
|
||||
try(listener)
|
||||
end
|
||||
|
||||
table.clear(batched)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Subscribes the listener to the changes of the given atoms.
|
||||
|
||||
@param atoms The atoms to listen to.
|
||||
@param listener The function to call when the atoms change.
|
||||
@param ref Optionally bind the lifetime of the listener to a value.
|
||||
]=]
|
||||
local function connect(atoms: Set<Atom<any>>, listener: () -> (), ref: unknown?)
|
||||
for atom in next, atoms do
|
||||
listeners[atom][listener] = ref or true
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
Unsubscribes the listener from every atom it was connected to.
|
||||
|
||||
@param atoms The atoms to stop listening to.
|
||||
@param listener The function to stop calling when the atoms change.
|
||||
]=]
|
||||
local function disconnect(atoms: Set<Atom<any>>, listener: () -> ())
|
||||
for atom in next, atoms do
|
||||
listeners[atom][listener] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
listeners = listeners,
|
||||
capturing = capturing,
|
||||
isAtom = isAtom,
|
||||
notify = notify,
|
||||
capture = capture,
|
||||
batch = batch,
|
||||
peek = peek,
|
||||
connect = connect,
|
||||
disconnect = disconnect,
|
||||
}
|
||||
|
|
@ -1,42 +1,36 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Selector<T> = types.Selector<T>
|
||||
local effectScope = require("./effectScope")
|
||||
local signals = require("./signals")
|
||||
local spawn = require("./spawn")
|
||||
|
||||
--[=[
|
||||
Subscribes to changes in the given atom or selector. The callback is
|
||||
called with the current state and the previous state immediately after a
|
||||
change occurs.
|
||||
|
||||
@param callback The atom or selector to subscribe to.
|
||||
@param listener The function to call when the state changes.
|
||||
@return A function that unsubscribes the callback.
|
||||
]=]
|
||||
local function subscribe<T>(callback: Selector<T>, listener: (state: T, prev: T) -> ()): () -> ()
|
||||
local dependencies, state = store.capture(callback)
|
||||
local disconnected = false
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function handler()
|
||||
local prevState = state
|
||||
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
||||
local current: T
|
||||
local mount = true
|
||||
local dispose: () -> ()
|
||||
|
||||
store.disconnect(dependencies, handler)
|
||||
dependencies, state = store.capture(callback)
|
||||
dispose = effectScope(function()
|
||||
effect(function()
|
||||
local value = selector()
|
||||
|
||||
if not disconnected then
|
||||
store.connect(dependencies, handler)
|
||||
end
|
||||
if mount then
|
||||
current = value
|
||||
mount = false
|
||||
else
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
if state ~= prevState then
|
||||
listener(state, prevState)
|
||||
end
|
||||
end
|
||||
|
||||
store.connect(dependencies, handler)
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous)
|
||||
resumeTracking()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
return function()
|
||||
if not disconnected then
|
||||
disconnected = true
|
||||
store.disconnect(dependencies, handler)
|
||||
end
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
357
packages/charm/src/system.luau
Normal file
357
packages/charm/src/system.luau
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
--[=[
|
||||
alien-signals 2.0.1 implementation adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
|
||||
export type ReactiveNode = {
|
||||
deps: Link?,
|
||||
depsTail: Link?,
|
||||
subs: Link?,
|
||||
subsTail: Link?,
|
||||
flags: ReactiveFlags,
|
||||
}
|
||||
|
||||
export type Link = {
|
||||
dep: ReactiveNode,
|
||||
sub: ReactiveNode,
|
||||
prevSub: Link?,
|
||||
nextSub: Link?,
|
||||
prevDep: Link?,
|
||||
nextDep: Link?,
|
||||
}
|
||||
|
||||
type Stack<T> = {
|
||||
value: T,
|
||||
prev: Stack<T>?,
|
||||
}
|
||||
|
||||
export type ReactiveFlags = number
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local RECURSED_CHECK = 0b0000100
|
||||
local RECURSED = 0b0001000
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
|
||||
local ReactiveFlags = {
|
||||
None = NONE,
|
||||
Mutable = MUTABLE,
|
||||
Watching = WATCHING,
|
||||
RecursedCheck = RECURSED_CHECK,
|
||||
Recursed = RECURSED,
|
||||
Dirty = DIRTY,
|
||||
Pending = PENDING,
|
||||
}
|
||||
|
||||
local function createReactiveSystem(props: {
|
||||
update: (sub: ReactiveNode) -> boolean,
|
||||
notify: (sub: ReactiveNode) -> (),
|
||||
unwatched: (sub: ReactiveNode) -> (),
|
||||
})
|
||||
local update = props.update
|
||||
local notify = props.notify
|
||||
local unwatched = props.unwatched
|
||||
|
||||
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
||||
local depsTail = sub.depsTail
|
||||
if depsTail then
|
||||
local link = sub.deps :: Link
|
||||
repeat
|
||||
if link == checkLink then
|
||||
return true
|
||||
end
|
||||
if link == depsTail then
|
||||
break
|
||||
end
|
||||
link = link.nextDep :: Link
|
||||
until not link
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function link(dep: ReactiveNode, sub: ReactiveNode): ()
|
||||
local prevDep = sub.depsTail
|
||||
if prevDep and prevDep.dep == dep then
|
||||
return
|
||||
end
|
||||
local nextDep: Link?
|
||||
local recursedCheck = bit32.band(sub.flags, RECURSED_CHECK) ~= 0
|
||||
if recursedCheck then
|
||||
nextDep = if prevDep then prevDep.nextDep else sub.deps
|
||||
if nextDep and nextDep.dep == dep then
|
||||
sub.depsTail = nextDep
|
||||
return
|
||||
end
|
||||
end
|
||||
local prevSub = dep.subsTail
|
||||
if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then
|
||||
return
|
||||
end
|
||||
local newLink: Link = {
|
||||
dep = dep,
|
||||
sub = sub,
|
||||
prevDep = prevDep,
|
||||
nextDep = nextDep,
|
||||
prevSub = prevSub,
|
||||
}
|
||||
sub.depsTail = newLink
|
||||
dep.subsTail = newLink
|
||||
if nextDep then
|
||||
nextDep.prevDep = newLink
|
||||
end
|
||||
if prevDep then
|
||||
prevDep.nextDep = newLink
|
||||
else
|
||||
sub.deps = newLink
|
||||
end
|
||||
if prevSub then
|
||||
prevSub.nextSub = newLink
|
||||
else
|
||||
dep.subs = newLink
|
||||
end
|
||||
end
|
||||
|
||||
local function unlink(link: Link, sub: ReactiveNode?): Link?
|
||||
local sub = sub or link.sub
|
||||
local dep = link.dep
|
||||
local prevDep = link.prevDep
|
||||
local nextDep = link.nextDep
|
||||
local nextSub = link.nextSub
|
||||
local prevSub = link.prevSub
|
||||
if nextDep then
|
||||
nextDep.prevDep = prevDep
|
||||
else
|
||||
sub.depsTail = prevDep
|
||||
end
|
||||
if prevDep then
|
||||
prevDep.nextDep = nextDep
|
||||
else
|
||||
sub.deps = nextDep
|
||||
end
|
||||
if nextSub then
|
||||
nextSub.prevSub = prevSub
|
||||
else
|
||||
dep.subsTail = prevSub
|
||||
end
|
||||
if prevSub then
|
||||
prevSub.nextSub = nextSub
|
||||
else
|
||||
dep.subs = nextSub
|
||||
if not nextSub then
|
||||
unwatched(dep)
|
||||
end
|
||||
end
|
||||
return nextDep
|
||||
end
|
||||
|
||||
local function propagate(link: Link): ()
|
||||
local next = link.nextSub
|
||||
local stack: Stack<Link?>?
|
||||
|
||||
repeat
|
||||
local sub = link.sub
|
||||
|
||||
local flags = sub.flags
|
||||
|
||||
if bit32.band(flags, bit32.bor(MUTABLE, WATCHING)) ~= 0 then
|
||||
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||
sub.flags = bit32.bor(flags, PENDING)
|
||||
elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||
flags = NONE
|
||||
elseif bit32.band(flags, RECURSED_CHECK) == 0 then
|
||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||
elseif bit32.band(flags, bit32.bor(DIRTY, PENDING)) == 0 and isValidLink(link, sub) then
|
||||
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
|
||||
flags = bit32.band(flags, MUTABLE)
|
||||
else
|
||||
-- selene: allow(if_same_then_else)
|
||||
flags = NONE
|
||||
end
|
||||
|
||||
if bit32.band(flags, WATCHING) ~= 0 then
|
||||
notify(sub)
|
||||
end
|
||||
|
||||
if bit32.band(flags, MUTABLE) ~= 0 then
|
||||
local subSubs = sub.subs
|
||||
if subSubs then
|
||||
link = subSubs
|
||||
if subSubs.nextSub then
|
||||
stack = { value = next, prev = stack }
|
||||
next = link.nextSub
|
||||
end
|
||||
continue
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
link = next :: Link
|
||||
|
||||
if link then
|
||||
next = link.nextSub
|
||||
continue
|
||||
end
|
||||
|
||||
local continueTop = false
|
||||
|
||||
while stack do
|
||||
link = stack.value :: Link
|
||||
stack = stack.prev
|
||||
if link then
|
||||
next = link.nextSub
|
||||
continueTop = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if continueTop then
|
||||
continue
|
||||
end
|
||||
|
||||
break
|
||||
until false
|
||||
end
|
||||
|
||||
local function startTracking(sub: ReactiveNode): ()
|
||||
sub.depsTail = nil
|
||||
sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK)
|
||||
end
|
||||
|
||||
local function endTracking(sub: ReactiveNode): ()
|
||||
local depsTail = sub.depsTail
|
||||
local toRemove = if depsTail then depsTail.nextDep else sub.deps
|
||||
while toRemove do
|
||||
toRemove = unlink(toRemove, sub)
|
||||
end
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(RECURSED_CHECK))
|
||||
end
|
||||
|
||||
local function shallowPropagate(link: Link): ()
|
||||
repeat
|
||||
local sub = link.sub
|
||||
local subFlags = sub.flags
|
||||
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then
|
||||
sub.flags = bit32.bor(subFlags, DIRTY)
|
||||
if bit32.band(subFlags, WATCHING) ~= 0 then
|
||||
notify(sub)
|
||||
end
|
||||
end
|
||||
link = link.nextSub :: Link
|
||||
until not link
|
||||
end
|
||||
|
||||
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
||||
local stack: Stack<Link>?
|
||||
local checkDepth = 0
|
||||
|
||||
repeat
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
|
||||
local dirty = false
|
||||
|
||||
if bit32.band(sub.flags, DIRTY) ~= 0 then
|
||||
dirty = true
|
||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
||||
if update(dep) then
|
||||
local subs = dep.subs :: Link
|
||||
if subs.nextSub then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
dirty = true
|
||||
end
|
||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
||||
if link.nextSub or link.prevSub then
|
||||
stack = { value = link, prev = stack }
|
||||
end
|
||||
link = dep.deps :: Link
|
||||
sub = dep
|
||||
checkDepth += 1
|
||||
continue
|
||||
end
|
||||
|
||||
if not dirty and link.nextDep then
|
||||
link = link.nextDep
|
||||
continue
|
||||
end
|
||||
|
||||
local continueTop = false
|
||||
|
||||
while checkDepth ~= 0 do
|
||||
checkDepth -= 1
|
||||
local firstSub = sub.subs :: Link
|
||||
local hasMultipleSubs = firstSub.nextSub ~= nil
|
||||
if hasMultipleSubs then
|
||||
link = (stack :: Stack<Link>).value
|
||||
stack = (stack :: Stack<Link>).prev
|
||||
else
|
||||
link = firstSub
|
||||
end
|
||||
if dirty then
|
||||
if update(sub) then
|
||||
if hasMultipleSubs then
|
||||
shallowPropagate(firstSub)
|
||||
end
|
||||
sub = link.sub
|
||||
continue
|
||||
end
|
||||
else
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
|
||||
end
|
||||
sub = link.sub
|
||||
if link.nextDep then
|
||||
link = link.nextDep
|
||||
continueTop = true
|
||||
break
|
||||
end
|
||||
dirty = false
|
||||
end
|
||||
|
||||
if continueTop then
|
||||
continue
|
||||
end
|
||||
|
||||
return dirty
|
||||
until false
|
||||
end
|
||||
|
||||
return {
|
||||
link = link,
|
||||
unlink = unlink,
|
||||
propagate = propagate,
|
||||
checkDirty = checkDirty,
|
||||
endTracking = endTracking,
|
||||
startTracking = startTracking,
|
||||
shallowPropagate = shallowPropagate,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
ReactiveFlags = ReactiveFlags,
|
||||
createReactiveSystem = createReactiveSystem,
|
||||
}
|
||||
|
|
@ -1,43 +1,9 @@
|
|||
export type Set<T> = { [T]: true }
|
||||
export type Selector<T = any> = () -> T
|
||||
|
||||
export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))
|
||||
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
||||
|
||||
--[=[
|
||||
A primitive state container that can be read from and written to. When the
|
||||
state changes, all subscribers are notified.
|
||||
|
||||
@param state The next state or a function that produces the next state.
|
||||
@return The current state, if no arguments are provided.
|
||||
]=]
|
||||
export type Atom<T> = (state: (T | (T) -> T)?) -> T
|
||||
|
||||
--[=[
|
||||
A function that depends on one or more atoms and produces a state. Can be
|
||||
used to derive state from atoms.
|
||||
|
||||
@template State The type of the state.
|
||||
@return The current state.
|
||||
]=]
|
||||
export type Selector<T> = () -> T
|
||||
|
||||
--[=[
|
||||
Optional configuration for creating an atom.
|
||||
]=]
|
||||
export type AtomOptions<T> = {
|
||||
--[=[
|
||||
A function that determines whether the state has changed. By default,
|
||||
a strict equality check (`===`) is used.
|
||||
]=]
|
||||
equals: (prev: T, next: T) -> boolean,
|
||||
export type AtomOptions<T = any> = {
|
||||
equals: ((prev: T, next: T) -> boolean)?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
A payload that can be sent from the server to the client to synchronize
|
||||
state between the two.
|
||||
]=]
|
||||
export type SyncPayload = {
|
||||
type: "patch" | "init",
|
||||
data: { [any]: any },
|
||||
}
|
||||
|
||||
return nil
|
||||
return {}
|
||||
|
|
|
|||
18
packages/charm/src/untracked.luau
Normal file
18
packages/charm/src/untracked.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||
pauseTracking()
|
||||
local success, result = pcall(callback, ...)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return untracked
|
||||
4
packages/react/src/index.d.ts
vendored
4
packages/react/src/index.d.ts
vendored
|
|
@ -1,5 +1,3 @@
|
|||
import { Selector } from "@rbxts/charm";
|
||||
|
||||
/**
|
||||
* A hook that subscribes to changes in the given atom or selector. The
|
||||
* component is re-rendered whenever the state changes.
|
||||
|
|
@ -12,4 +10,4 @@ import { Selector } from "@rbxts/charm";
|
|||
* @param dependencies An array of values that the subscription depends on.
|
||||
* @returns The current state.
|
||||
*/
|
||||
export function useAtom<State>(callback: Selector<State>, dependencies?: unknown[]): State;
|
||||
export function useAtom<State>(callback: () => State, dependencies?: unknown[]): State;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local React = if script.Parent:FindFirstChild("react")
|
||||
then require(script.Parent.react) :: never
|
||||
then require((script :: any).Parent.react) :: never
|
||||
else require(script.Parent.React)
|
||||
|
||||
local listen = Charm.listen
|
||||
local useEffect = React.useEffect
|
||||
local useState = React.useState
|
||||
|
||||
--[=[
|
||||
A hook that subscribes to changes in the given atom or selector. The
|
||||
component is re-rendered whenever the state changes.
|
||||
|
|
@ -16,11 +20,10 @@ local React = if script.Parent:FindFirstChild("react")
|
|||
@return The current state.
|
||||
]=]
|
||||
local function useAtom<State>(callback: Charm.Selector<State>, dependencies: { any }?): State
|
||||
local state, setState = React.useState(callback)
|
||||
local state, setState = useState(callback)
|
||||
|
||||
React.useEffect(function()
|
||||
setState(callback())
|
||||
return Charm.subscribe(callback, setState)
|
||||
useEffect(function()
|
||||
return listen(callback, setState)
|
||||
end, dependencies or {})
|
||||
|
||||
return state
|
||||
|
|
|
|||
4
packages/vide/src/index.d.ts
vendored
4
packages/vide/src/index.d.ts
vendored
|
|
@ -1,9 +1,7 @@
|
|||
import { Selector } from "@rbxts/charm";
|
||||
|
||||
/**
|
||||
* Subscribes to the state of an atom and returns a Vide source.
|
||||
*
|
||||
* @param callback The atom or selector to subscribe to.
|
||||
* @returns The reactive source.
|
||||
*/
|
||||
export function useAtom<T>(callback: Selector<T>): () => T;
|
||||
export function useAtom<T>(callback: () => T): () => T;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local Vide = if script.Parent:FindFirstChild("vide")
|
||||
then require(script.Parent.vide.src) :: never
|
||||
then require((script :: any).Parent.vide.src) :: never
|
||||
else require(script.Parent.Vide)
|
||||
|
||||
local subscribe = Charm.subscribe
|
||||
local source = Vide.source
|
||||
local cleanup = Vide.cleanup
|
||||
|
||||
--[=[
|
||||
Subscribes to the state of an atom and returns a Vide source.
|
||||
|
||||
|
|
@ -10,13 +14,9 @@ local Vide = if script.Parent:FindFirstChild("vide")
|
|||
@return The reactive source.
|
||||
]=]
|
||||
local function useAtom<State>(callback: () -> State): () -> State
|
||||
local state = Vide.source(callback())
|
||||
local unsubscribe = Charm.subscribe(callback, function(value)
|
||||
task.spawn(state :: (State) -> State, value)
|
||||
end)
|
||||
|
||||
Vide.cleanup(unsubscribe)
|
||||
|
||||
local state = source(callback())
|
||||
local dispose = subscribe(callback, state)
|
||||
cleanup(dispose)
|
||||
return state
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue