docs: rewrite documentation (#25)

* docs: rewrite documentation

* refactor: rename `Molecule` to `Selector`

* refactor: replace all references to molecules

* docs: rephrase intro

* docs: remove italics

* docs: fix typo

* docs: update atom module names

* docs: link example, add file names
This commit is contained in:
richard 2024-10-25 19:53:46 -07:00 • committed by GitHub
parent 833d5f8e86
commit 73ae0e6de4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 399 additions and 350 deletions

View file

@ -1,4 +1,4 @@
import { Atom, Molecule } from "@rbxts/charm";
import { Atom, Selector } from "@rbxts/charm";
export = CharmSync;
export as namespace CharmSync;
@ -8,13 +8,18 @@ type Cleanup = () => void;
declare namespace CharmSync {
type AtomMap = Record<string, Atom<any>>;
type MoleculeMap = Record<string, Molecule<any>>;
/**
* @deprecated Use `SelectorMap` instead.
*/
type MoleculeMap = Record<string, Selector<any>>;
type SelectorMap = Record<string, Selector<any>>;
/**
* Recursively infers the type of the state produced by a map of molecules.
* Infers the type of the return values produced by a map of functions.
*/
type StateOfMap<T> = {
[P in keyof T]: T[P] extends Molecule<infer State> ? State : never;
[P in keyof T]: T[P] extends Selector<infer State> ? State : never;
};
/**
@ -45,7 +50,7 @@ declare namespace CharmSync {
* @param options The atoms to synchronize with the client.
* @returns A `ServerSyncer` object.
*/
function server<Molecules extends MoleculeMap>(options: ServerOptions<Molecules>): ServerSyncer<Molecules>;
function server<Selectors extends SelectorMap>(options: ServerOptions<Selectors>): ServerSyncer<Selectors>;
/**
* Checks whether a value is `None`. If `true`, the value is scheduled to be
@ -90,9 +95,9 @@ declare namespace CharmSync {
* A payload that can be sent from the server to the client to synchronize
* state between the two.
*/
type SyncPayload<Molecules extends MoleculeMap> =
| { type: "init"; data: StateOfMap<Molecules> }
| { type: "patch"; data: SyncPatch<StateOfMap<Molecules>> };
type SyncPayload<Selectors extends SelectorMap> =
| { type: "init"; data: StateOfMap<Selectors> }
| { type: "patch"; data: SyncPatch<StateOfMap<Selectors>> };
interface ClientOptions<Atoms extends AtomMap> {
/**
@ -106,11 +111,11 @@ declare namespace CharmSync {
ignoreUnhydrated?: boolean;
}
interface ServerOptions<Molecules extends MoleculeMap> {
interface ServerOptions<Selectors extends SelectorMap> {
/**
* The atoms to synchronize with the client.
*/
atoms: Molecules;
atoms: Selectors;
/**
* The interval at which to send patches to the client, in seconds.
* Defaults to `0` (patches are sent up to once per frame). Set to a
@ -139,7 +144,7 @@ declare namespace CharmSync {
sync(...payloads: SyncPayload<Atoms>[]): void;
}
interface ServerSyncer<Molecules extends MoleculeMap> {
interface ServerSyncer<Selectors extends SelectorMap> {
/**
* Sets up a subscription to each atom that schedules a patch to be sent to
* the client whenever the state changes. When a change occurs, the `callback`
@ -151,7 +156,7 @@ declare namespace CharmSync {
* @param callback The function to call when the state changes.
* @returns A cleanup function that unsubscribes all listeners.
*/
connect(callback: (player: Player, ...payloads: SyncPayload<Molecules>[]) => void): Cleanup;
connect(callback: (player: Player, ...payloads: SyncPayload<Selectors>[]) => void): Cleanup;
/**
* Hydrates the client's state with the server's state. This should be
* called when a player joins the game and requires the server's state.

View file

@ -1,21 +1,20 @@
local atom = require(script.Parent.atom)
local store = require(script.Parent.store)
local types = require(script.Parent.types)
type Atom<T> = types.Atom<T>
type AtomOptions<T> = types.AtomOptions<T>
type Molecule<T> = types.Molecule<T>
type Selector<T> = types.Selector<T>
--[=[
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 molecule.
@param molecule The function that produces the state.
the same atoms.
@param callback The function that produces the state.
@param options Optional configuration.
@return A new read-only atom.
]=]
local function computed<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Molecule<T>
local dependencies, state = store.capture(molecule)
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" })
@ -24,7 +23,7 @@ local function computed<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Mol
if computedAtom then
store.disconnect(dependencies, listener)
dependencies, state = store.capture(molecule)
dependencies, state = store.capture(callback)
store.connect(dependencies, listener, computedAtom)
computedAtom(state)
end

View file

@ -9,7 +9,7 @@ type Cleanup = () -> ()
@param callback The function to run.
@return A function that unsubscribes the callback.
]=]
local function effect(callback: () -> (() -> ())?): () -> ()
local function effect(callback: () -> Cleanup?): Cleanup
local dependencies, cleanup = store.capture(callback)
local disconnected = false

View file

@ -17,7 +17,7 @@ declare namespace Charm {
* @param state The next state or a function that produces the next state.
* @returns The current state, if no arguments are provided.
*/
interface Atom<State> extends Molecule<State> {
interface Atom<State> extends Selector<State> {
/**
* @deprecated This property is not meant to be accessed directly.
*/
@ -26,21 +26,29 @@ declare namespace Charm {
* @param state The next state or a function that produces the next state.
* @returns The current state, if no arguments are provided.
*/
(state: State | ((prev: State) => State)): void;
(state?: State | ((prev: State) => State)): State;
}
/**
* A function that depends on one or more atoms and produces a state. Can be
* A function that depends on one or more atoms and produces a value. Can be
* used to derive state from atoms.
*
* @returns The current state.
*/
type Molecule<State> = () => State;
type Selector<State> = () => State;
/**
* Infers the type of the state produced by the given molecule.
* A function that depends on one or more atoms and produces a value. Can be
* used to derive state from atoms.
*
* @returns The current state.
*/
type StateOf<T> = T extends Molecule<infer State> ? State : never;
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;
interface AtomOptions<State> {
/**
@ -66,24 +74,24 @@ declare namespace Charm {
/**
* 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 molecule.
* the same atoms.
*
* @param molecule The function that produces the state.
* @param callback The function that produces the state.
* @param options Optional configuration.
* @returns A new read-only atom.
*/
function computed<State>(molecule: Molecule<State>, options?: AtomOptions<State>): Molecule<State>;
function computed<State>(callback: Selector<State>, options?: AtomOptions<State>): Selector<State>;
/**
* Subscribes to changes in the given atom or molecule. The callback is
* 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 molecule The atom or molecule to subscribe to.
* @param callback The function to call when the state changes.
* @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>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
function subscribe<State>(callback: Selector<State>, listener: (state: State, prev: State) => void): Cleanup;
/**
* Runs the given callback immediately and whenever any atom it depends on
@ -98,11 +106,11 @@ declare namespace Charm {
* Returns the result of the function without subscribing to changes. If a
* non-function value is provided, it is returned as is.
*
* @param molecule The atom or molecule to get the state of.
* @param args Arguments to pass to the molecule.
* @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[]>(molecule: State | ((...args: Args) => State), ...args: Args): State;
function peek<State, Args extends unknown[]>(callback: State | ((...args: Args) => State), ...args: Args): State;
/**
* Returns whether the given value is an atom.
@ -124,13 +132,14 @@ declare namespace Charm {
* 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 molecule The function to run.
* @param callback The function to run.
* @returns A tuple containing the captured atoms and the result of the function.
*/
function capture<State>(molecule: Molecule<State>): LuaTuple<[dependencies: Set<Atom<unknown>>, state: 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.
*/
@ -141,17 +150,17 @@ declare namespace Charm {
* cleans up the instance when the item is removed. Returns a cleanup function
* that unsubscribes all instances.
*
* @param molecule The atom or molecule to observe.
* @param callback The atom or selector to observe.
* @param factory The function that tracks the lifecycle of each item.
* @returns A function that unsubscribes all instances.
*/
function observe<Item>(
molecule: Molecule<readonly Item[]>,
callback: Selector<readonly Item[]>,
factory: (item: Item, index: number) => Cleanup | void,
): Cleanup;
function observe<Key, Item>(
molecule: Molecule<AnyMap<Key, Item>>,
callback: Selector<AnyMap<Key, Item>>,
factory: (item: Item, key: Key) => Cleanup | void,
): Cleanup;
@ -161,22 +170,22 @@ declare namespace Charm {
* When the atom changes, the `mapper` is called for each entry in the state
* to compute the new state.
*
* @param molecule The atom or molecule to map.
* @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>(
molecule: Molecule<readonly V0[]>,
callback: Selector<readonly V0[]>,
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
): Molecule<ReadonlyMap<K1, V1>>;
): Selector<ReadonlyMap<K1, V1>>;
function mapped<V0, V1>(
molecule: Molecule<readonly V0[]>,
callback: Selector<readonly V0[]>,
mapper: (value: V0, index: number) => V1,
): Molecule<readonly V1[]>;
): Selector<readonly V1[]>;
function mapped<K0, V0, K1 = K0, V1 = V0>(
molecule: Molecule<AnyMap<K0, V0>>,
callback: Selector<AnyMap<K0, V0>>,
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
): Molecule<ReadonlyMap<K1, V1>>;
): Selector<ReadonlyMap<K1, V1>>;
}

View file

@ -8,7 +8,8 @@ local subscribe = require(script.subscribe)
local types = require(script.types)
export type Atom<State> = types.Atom<State>
export type Molecule<State> = types.Molecule<State>
export type Selector<State> = types.Selector<State>
export type Molecule<State> = types.Selector<State>
export type SyncPayload = types.SyncPayload
return {

View file

@ -3,12 +3,12 @@ local effect = require(script.Parent.effect)
local store = require(script.Parent.store)
local types = require(script.Parent.types)
type Atom<T> = types.Atom<T>
type Molecule<T> = types.Molecule<T>
type Selector<T> = types.Selector<T>
type Map =
(<K0, V0, K1, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1)) -> Molecule<{ [K1]: V1 }>)
& (<K0, V0, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Molecule<{ [K0]: V1 }>)
& (<K0, V0, K1, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Molecule<{ [K1]: V1 }>)
(<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 }>)
--[=[
Maps each entry in the atom's state to a new key-value pair. If the `mapper`
@ -16,11 +16,11 @@ type Map =
When the atom changes, the `mapper` is called for each entry in the state
to compute the new state.
@param molecule The atom or molecule to map.
@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>(molecule: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Molecule<{ [K1]: V1 }>
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 } = {}
@ -31,7 +31,7 @@ local function mapped<K0, V0, K1, V1>(molecule: Molecule<{ [K0]: V0 }>, mapper:
return unsubscribe()
end
local items = molecule()
local items = callback()
local mappedItems = table.clone(store.peek(mappedAtomRef.current))
local mappedKeys = {}

View file

@ -1,6 +1,6 @@
local effect = require(script.Parent.effect)
local types = require(script.Parent.types)
type Molecule<T> = types.Molecule<T>
type Selector<T> = types.Selector<T>
local function noop() end
@ -9,15 +9,15 @@ local function noop() end
cleans up the instance when the item is removed. Returns a cleanup function
that unsubscribes all instances.
@param molecule The atom or molecule to observe.
@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>(molecule: Molecule<{ [K]: V }>, factory: (value: V, key: K) -> (() -> ())?): () -> ()
local function observe<K, V>(callback: Selector<{ [K]: V }>, factory: (value: V, key: K) -> (() -> ())?): () -> ()
local connections: { [K]: () -> () } = {}
local unsubscribe = effect(function()
local state = molecule()
local state = callback()
for key, disconnect in next, connections do
if state[key] == nil then

View file

@ -100,8 +100,8 @@ end
Returns the result of the function without subscribing to changes. If a
non-function value is provided, it is returned as is.
@param molecule The atom or molecule to get the state of.
@param args Arguments to pass to the molecule.
@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
@ -128,12 +128,12 @@ 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 molecule The function to run.
@param callback The function to run.
@return A tuple containing the captured atoms and the result of the function.
]=]
local function capture<T>(molecule: () -> T): (Set<Atom<any>>, T)
if listeners[molecule] then
return { [molecule] = true }, peek(molecule)
local function capture<T>(callback: () -> T): (Set<Atom<any>>, T)
if listeners[callback] then
return { [callback] = true }, peek(callback)
end
local dependencies: Set<Atom<any>> = {}
@ -141,7 +141,7 @@ local function capture<T>(molecule: () -> T): (Set<Atom<any>>, T)
capturing.index += 1
capturing.stack[capturing.index] = dependencies
local result = try(molecule, function()
local result = try(callback, function()
capturing.stack[capturing.index] = nil
capturing.index -= 1
end)

View file

@ -1,41 +1,41 @@
local store = require(script.Parent.store)
local types = require(script.Parent.types)
type Molecule<T> = types.Molecule<T>
type Selector<T> = types.Selector<T>
--[=[
Subscribes to changes in the given atom or molecule. The callback is
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 molecule The atom or molecule to subscribe to.
@param callback The function to call when the state changes.
@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>(molecule: Molecule<T>, callback: (state: T, prev: T) -> ()): () -> ()
local dependencies, state = store.capture(molecule)
local function subscribe<T>(callback: Selector<T>, listener: (state: T, prev: T) -> ()): () -> ()
local dependencies, state = store.capture(callback)
local disconnected = false
local function listener()
local function handler()
local prevState = state
store.disconnect(dependencies, listener)
dependencies, state = store.capture(molecule)
store.disconnect(dependencies, handler)
dependencies, state = store.capture(callback)
if not disconnected then
store.connect(dependencies, listener)
store.connect(dependencies, handler)
end
if state ~= prevState then
callback(state, prevState)
listener(state, prevState)
end
end
store.connect(dependencies, listener)
store.connect(dependencies, handler)
return function()
if not disconnected then
disconnected = true
store.disconnect(dependencies, listener)
store.disconnect(dependencies, handler)
end
end
end

View file

@ -9,7 +9,7 @@ export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k"
@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> = (() -> T) & (state: T | (T) -> T) -> T
export type Atom<T> = (state: (T | (T) -> T)?) -> T
--[=[
A function that depends on one or more atoms and produces a state. Can be
@ -18,7 +18,7 @@ export type Atom<T> = (() -> T) & (state: T | (T) -> T) -> T
@template State The type of the state.
@return The current state.
]=]
export type Molecule<T> = () -> T
export type Selector<T> = () -> T
--[=[
Optional configuration for creating an atom.

View file

@ -1,15 +1,15 @@
import { Molecule } from "@rbxts/charm";
import { Selector } from "@rbxts/charm";
/**
* A hook that subscribes to changes in the given atom or molecule. The
* A hook that subscribes to changes in the given atom or selector. The
* component is re-rendered whenever the state changes.
*
* If the `dependencies` array is provided, the subscription to the atom or
* molecule is re-created whenever the dependencies change. Otherwise, the
* selector is re-created whenever the dependencies change. Otherwise, the
* subscription is created once when the component is mounted.
*
* @param molecule The atom or molecule to subscribe to.
* @param callback The atom or selector to subscribe to.
* @param dependencies An array of values that the subscription depends on.
* @returns The current state.
*/
export function useAtom<State>(molecule: Molecule<State>, dependencies?: unknown[]): State;
export function useAtom<State>(callback: Selector<State>, dependencies?: unknown[]): State;

View file

@ -4,23 +4,23 @@ local React = if script.Parent:FindFirstChild("react")
else require(script.Parent.React)
--[=[
A hook that subscribes to changes in the given atom or molecule. The
A hook that subscribes to changes in the given atom or selector. The
component is re-rendered whenever the state changes.
If the `dependencies` array is provided, the subscription to the atom or
molecule is re-created whenever the dependencies change. Otherwise, the
selector is re-created whenever the dependencies change. Otherwise, the
subscription is created once when the component is mounted.
@param molecule The atom or molecule to subscribe to.
@param callback The atom or selector to subscribe to.
@param dependencies An array of values that the subscription depends on.
@return The current state.
]=]
local function useAtom<State>(molecule: Charm.Molecule<State>, dependencies: { any }?): State
local state, setState = React.useState(molecule)
local function useAtom<State>(callback: Charm.Selector<State>, dependencies: { any }?): State
local state, setState = React.useState(callback)
React.useEffect(function()
setState(molecule())
return Charm.subscribe(molecule, setState)
setState(callback())
return Charm.subscribe(callback, setState)
end, dependencies or {})
return state

View file

@ -1,9 +1,9 @@
import { Molecule } from "@rbxts/charm";
import { Selector } from "@rbxts/charm";
/**
* Subscribes to the state of an atom and returns a Vide source.
*
* @param molecule The atom or molecule to subscribe to.
* @param callback The atom or selector to subscribe to.
* @returns The reactive source.
*/
export function useAtom<T>(atom: Molecule<T>): () => T;
export function useAtom<T>(callback: Selector<T>): () => T;

View file

@ -6,12 +6,12 @@ local Vide = if script.Parent:FindFirstChild("vide")
--[=[
Subscribes to the state of an atom and returns a Vide source.
@param molecule The atom or molecule to subscribe to.
@param callback The atom or selector to subscribe to.
@return The reactive source.
]=]
local function useAtom<State>(molecule: () -> State): State
local state = Vide.source(molecule())
local unsubscribe = Charm.subscribe(molecule, function(value)
local function useAtom<State>(callback: () -> State): State
local state = Vide.source(callback())
local unsubscribe = Charm.subscribe(callback, function(value)
task.spawn(state, value)
end)