mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 21:01:38 +00:00
docs: add doc comments
This commit is contained in:
parent
df43a9be2e
commit
6a2241884a
17 changed files with 361 additions and 14 deletions
|
|
@ -1,9 +1,15 @@
|
||||||
local peek = require(script.Parent.peek)
|
|
||||||
local store = require(script.Parent.store)
|
local store = require(script.Parent.store)
|
||||||
local types = require(script.Parent.types)
|
local types = require(script.Parent.types)
|
||||||
type Atom<T> = types.Atom<T>
|
type Atom<T> = types.Atom<T>
|
||||||
type AtomOptions<T> = types.AtomOptions<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 function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||||
local equals = options and options.equals
|
local equals = options and options.equals
|
||||||
|
|
||||||
|
|
@ -16,7 +22,7 @@ local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
||||||
local nextState = peek(..., state)
|
local nextState = store.peek(..., state)
|
||||||
|
|
||||||
if state ~= nextState and not (equals and equals(state, nextState)) then
|
if state ~= nextState and not (equals and equals(state, nextState)) then
|
||||||
state = nextState
|
state = nextState
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@ type Atom<T> = types.Atom<T>
|
||||||
type AtomOptions<T> = types.AtomOptions<T>
|
type AtomOptions<T> = types.AtomOptions<T>
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<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.
|
||||||
|
@param options Optional configuration.
|
||||||
|
@return A new read-only atom.
|
||||||
|
]=]
|
||||||
local function computed<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Molecule<T>
|
local function computed<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Molecule<T>
|
||||||
local captured, state = store.capture(molecule)
|
local captured, state = store.capture(molecule)
|
||||||
local computedAtom = atom(state, options)
|
local computedAtom = atom(state, options)
|
||||||
|
|
|
||||||
190
src/index.d.ts
vendored
190
src/index.d.ts
vendored
|
|
@ -9,35 +9,138 @@ type AnyMap<K, V> =
|
||||||
| (K extends string | number | symbol ? { readonly [Key in K]: V } : never);
|
| (K extends string | number | symbol ? { readonly [Key in K]: V } : never);
|
||||||
|
|
||||||
declare namespace Charm {
|
declare namespace Charm {
|
||||||
|
/**
|
||||||
|
* A primitive state container that can be read from and written to. When the
|
||||||
|
* state changes, all subscribers are notified.
|
||||||
|
*
|
||||||
|
* @template State The type of the state.
|
||||||
|
* @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 Molecule<State> {
|
||||||
|
/**
|
||||||
|
* @deprecated This property is not meant to be accessed directly.
|
||||||
|
*/
|
||||||
readonly __nominal: unique symbol;
|
readonly __nominal: unique symbol;
|
||||||
|
/**
|
||||||
|
* @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)): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @returns The current state.
|
||||||
|
*/
|
||||||
type Molecule<State> = () => State;
|
type Molecule<State> = () => State;
|
||||||
|
|
||||||
interface AtomOptions<State> {
|
interface AtomOptions<State> {
|
||||||
|
/**
|
||||||
|
* A function that determines whether the state has changed. By default,
|
||||||
|
* a strict equality check (`===`) is used.
|
||||||
|
*/
|
||||||
equals?: (prev: State, next: State) => boolean;
|
equals?: (prev: State, next: State) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new atom with the given state.
|
||||||
|
*
|
||||||
|
* @template State The type of the state.
|
||||||
|
* @param state The initial state.
|
||||||
|
* @param options Optional configuration.
|
||||||
|
* @returns A new atom.
|
||||||
|
*/
|
||||||
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
|
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param options Optional configuration.
|
||||||
|
* @returns A new read-only atom.
|
||||||
|
*/
|
||||||
function computed<State>(molecule: Molecule<State>, options?: AtomOptions<State>): Molecule<State>;
|
function computed<State>(molecule: Molecule<State>, options?: AtomOptions<State>): Molecule<State>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to changes in the given atom or molecule. 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.
|
||||||
|
* @returns A function that unsubscribes the callback.
|
||||||
|
*/
|
||||||
function subscribe<State>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
|
function subscribe<State>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given callback immediately and whenever any atom it depends on
|
||||||
|
* changes. Returns a cleanup function that unsubscribes the callback.
|
||||||
|
*
|
||||||
|
* Note that dependencies are only evaluated once when the effect is created,
|
||||||
|
* conditional dependencies may not work as expected.
|
||||||
|
*
|
||||||
|
* @param callback The function to run.
|
||||||
|
* @returns A function that unsubscribes the callback.
|
||||||
|
*/
|
||||||
function effect(callback: () => void): Cleanup;
|
function effect(callback: () => 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 molecule The atom or molecule to get the state of.
|
||||||
|
* @param args Arguments to pass to the molecule.
|
||||||
|
* @returns The current state.
|
||||||
|
*/
|
||||||
function peek<State, Args extends unknown[]>(molecule: State | ((...args: Args) => State), ...args: Args): State;
|
function peek<State, Args extends unknown[]>(molecule: 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 isAtom(value: unknown): value is Atom<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
function batch(callback: () => void): void;
|
function batch(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.
|
||||||
|
*
|
||||||
|
* @param molecule The function to run.
|
||||||
|
* @returns A tuple containing the captured atoms and the result of the function.
|
||||||
|
*/
|
||||||
function capture<State>(molecule: Molecule<State>): LuaTuple<[captured: Set<Atom<unknown>>, state: State]>;
|
function capture<State>(molecule: Molecule<State>): LuaTuple<[captured: Set<Atom<unknown>>, state: State]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all subscribers of the given atom that the state has changed.
|
||||||
|
*
|
||||||
|
* @param atom The atom to notify.
|
||||||
|
*/
|
||||||
function notify(atom: Atom<unknown>): void;
|
function notify(atom: Atom<unknown>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 molecule The atom or molecule to observe.
|
||||||
|
* @param factory The function that tracks the lifecycle of each item.
|
||||||
|
* @returns A function that unsubscribes all instances.
|
||||||
|
*/
|
||||||
function observe<Item>(
|
function observe<Item>(
|
||||||
molecule: Molecule<readonly Item[]>,
|
molecule: Molecule<readonly Item[]>,
|
||||||
factory: (item: Item, index: number) => Cleanup | void,
|
factory: (item: Item, index: number) => Cleanup | void,
|
||||||
|
|
@ -48,6 +151,16 @@ declare namespace Charm {
|
||||||
factory: (item: Item, key: Key) => Cleanup | void,
|
factory: (item: Item, key: Key) => Cleanup | void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 molecule The atom or molecule to map.
|
||||||
|
* @param mapper The function that maps each entry.
|
||||||
|
* @returns A new atom with the mapped state.
|
||||||
|
*/
|
||||||
function mapped<V0, K1, V1>(
|
function mapped<V0, K1, V1>(
|
||||||
molecule: Molecule<readonly V0[]>,
|
molecule: Molecule<readonly V0[]>,
|
||||||
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
||||||
|
|
@ -65,41 +178,110 @@ declare namespace Charm {
|
||||||
|
|
||||||
// React
|
// React
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that subscribes to changes in the given atom or molecule. 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
|
||||||
|
* subscription is created once when the component is mounted.
|
||||||
|
*
|
||||||
|
* @param molecule The atom or molecule to subscribe to.
|
||||||
|
* @param dependencies An array of values that the subscription depends on.
|
||||||
|
* @returns The current state.
|
||||||
|
*/
|
||||||
function useAtom<State>(molecule: Molecule<State>, dependencies?: unknown[]): State;
|
function useAtom<State>(molecule: Molecule<State>, dependencies?: unknown[]): State;
|
||||||
|
|
||||||
// Sync
|
/**
|
||||||
|
* Synchronizes state between the client and server. The server sends patches
|
||||||
|
* to the client, which applies them to its local state.
|
||||||
|
*/
|
||||||
const sync: {
|
const sync: {
|
||||||
|
/**
|
||||||
|
* Creates a `ClientSyncer` object that receives patches from the server and
|
||||||
|
* applies them to the local state.
|
||||||
|
*
|
||||||
|
* @param options The atoms to synchronize with the server.
|
||||||
|
* @returns A `ClientSyncer` object.
|
||||||
|
*/
|
||||||
client: <T extends Record<string, Atom<any>>>(options: ClientOptions<T>) => ClientSyncer<T>;
|
client: <T extends Record<string, Atom<any>>>(options: ClientOptions<T>) => ClientSyncer<T>;
|
||||||
|
/**
|
||||||
|
* Creates a `ServerSyncer` object that sends patches to the client and
|
||||||
|
* hydrates the client's state.
|
||||||
|
*
|
||||||
|
* @param options The atoms to synchronize with the client.
|
||||||
|
* @returns A `ServerSyncer` object.
|
||||||
|
*/
|
||||||
server: <T extends Record<string, Atom<any>>>(options: ServerOptions<T>) => ServerSyncer<T>;
|
server: <T extends Record<string, Atom<any>>>(options: ServerOptions<T>) => ServerSyncer<T>;
|
||||||
collect: (root: Instance) => Record<string, Atom<any>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special value that denotes the absence of a value. Used to represent
|
||||||
|
* undefined values in patches.
|
||||||
|
*/
|
||||||
interface None {
|
interface None {
|
||||||
__none: "__none";
|
readonly __none: "__none";
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncPatch<T> = {
|
type SyncPatch<T> = {
|
||||||
readonly [P in keyof T]?: (T[P] extends object ? SyncPatch<T[P]> : T[P]) | (T[P] extends undefined ? None : never);
|
readonly [P in keyof T]?: (T[P] extends object ? SyncPatch<T[P]> : T[P]) | (T[P] extends undefined ? None : never);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A payload that can be sent from the server to the client to synchronize
|
||||||
|
* state between the two.
|
||||||
|
*/
|
||||||
type SyncPayload<T> = { type: "init"; data: T } | { type: "patch"; data: SyncPatch<T> };
|
type SyncPayload<T> = { type: "init"; data: T } | { type: "patch"; data: SyncPatch<T> };
|
||||||
|
|
||||||
interface ClientOptions<T extends Record<string, Atom<any>>> {
|
interface ClientOptions<T extends Record<string, Atom<any>>> {
|
||||||
|
/**
|
||||||
|
* The atoms to synchronize with the server.
|
||||||
|
*/
|
||||||
atoms: T;
|
atoms: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerOptions<T extends Record<string, Atom<any>>> {
|
interface ServerOptions<T extends Record<string, Atom<any>>> {
|
||||||
|
/**
|
||||||
|
* The atoms to synchronize with the client.
|
||||||
|
*/
|
||||||
atoms: T;
|
atoms: T;
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* negative value to disable automatic syncing.
|
||||||
|
*/
|
||||||
interval?: number;
|
interval?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientSyncer<T extends Record<string, Atom<any>>> {
|
interface ClientSyncer<T extends Record<string, Atom<any>>> {
|
||||||
|
/**
|
||||||
|
* Applies a patch or initializes the state of the atoms with the given
|
||||||
|
* payload from the server.
|
||||||
|
*
|
||||||
|
* @param payload The patch or hydration payload to apply.
|
||||||
|
*/
|
||||||
sync(payload: SyncPayload<T>): void;
|
sync(payload: SyncPayload<T>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerSyncer<T extends Record<string, Atom<any>>> {
|
interface ServerSyncer<T extends Record<string, Atom<any>>> {
|
||||||
|
/**
|
||||||
|
* 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`
|
||||||
|
* is called with the player and the payload to send.
|
||||||
|
*
|
||||||
|
* Note that the `payload` object should not be mutated. If you need to
|
||||||
|
* modify the payload, apply the changes to a copy of the object.
|
||||||
|
*
|
||||||
|
* @param callback The function to call when the state changes.
|
||||||
|
* @returns A cleanup function that unsubscribes all listeners.
|
||||||
|
*/
|
||||||
connect(callback: (player: Player, payload: SyncPayload<T>) => void): Cleanup;
|
connect(callback: (player: Player, payload: SyncPayload<T>) => 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.
|
||||||
|
*
|
||||||
|
* @param player The player to hydrate.
|
||||||
|
*/
|
||||||
hydrate(player: Player): void;
|
hydrate(player: Player): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ local computed = require(script.computed)
|
||||||
local effect = require(script.effect)
|
local effect = require(script.effect)
|
||||||
local mapped = require(script.mapped)
|
local mapped = require(script.mapped)
|
||||||
local observe = require(script.observe)
|
local observe = require(script.observe)
|
||||||
local peek = require(script.peek)
|
|
||||||
local useAtom = require(script.react.useAtom)
|
local useAtom = require(script.react.useAtom)
|
||||||
local store = require(script.store)
|
local store = require(script.store)
|
||||||
local subscribe = require(script.subscribe)
|
local subscribe = require(script.subscribe)
|
||||||
|
|
@ -22,12 +21,12 @@ return {
|
||||||
effect = effect,
|
effect = effect,
|
||||||
mapped = mapped,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
peek = peek,
|
|
||||||
useAtom = useAtom,
|
useAtom = useAtom,
|
||||||
batch = store.batch,
|
batch = store.batch,
|
||||||
capture = store.capture,
|
capture = store.capture,
|
||||||
notify = store.notify,
|
|
||||||
isAtom = store.isAtom,
|
isAtom = store.isAtom,
|
||||||
|
notify = store.notify,
|
||||||
|
peek = store.peek,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
sync = sync,
|
sync = sync,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
local atom = require(script.Parent.atom)
|
local atom = require(script.Parent.atom)
|
||||||
local effect = require(script.Parent.effect)
|
local effect = require(script.Parent.effect)
|
||||||
local peek = require(script.Parent.peek)
|
local store = require(script.Parent.store)
|
||||||
local types = require(script.Parent.types)
|
local types = require(script.Parent.types)
|
||||||
type Atom<T> = types.Atom<T>
|
type Atom<T> = types.Atom<T>
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
@ -10,6 +10,16 @@ type Map =
|
||||||
& (<K0, V0, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Molecule<{ [K0]: 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: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Molecule<{ [K1]: V1 }>)
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
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 molecule The atom or molecule 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>(molecule: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Molecule<{ [K1]: V1 }>
|
||||||
local mappedAtom = atom({})
|
local mappedAtom = atom({})
|
||||||
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
||||||
|
|
@ -22,9 +32,10 @@ local function mapped<K0, V0, K1, V1>(molecule: Molecule<{ [K0]: V0 }>, mapper:
|
||||||
end
|
end
|
||||||
|
|
||||||
local items = molecule()
|
local items = molecule()
|
||||||
local mappedItems = table.clone(peek(mappedAtomRef.current))
|
local mappedItems = table.clone(store.peek(mappedAtomRef.current))
|
||||||
local mappedKeys = {}
|
local mappedKeys = {}
|
||||||
|
|
||||||
|
-- TODO: Only call mapper if the item has changed.
|
||||||
for key, item in items do
|
for key, item in items do
|
||||||
local newItem, newKey = mapper(item, key)
|
local newItem, newKey = mapper(item, key)
|
||||||
if newKey == nil then
|
if newKey == nil then
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@ local effect = require(script.Parent.effect)
|
||||||
local types = require(script.Parent.types)
|
local types = require(script.Parent.types)
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
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 molecule The atom or molecule 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>(molecule: Molecule<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> ()
|
||||||
local connections: { [K]: () -> () } = {}
|
local connections: { [K]: () -> () } = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,18 @@ local subscribe = require(script.Parent.Parent.subscribe)
|
||||||
local types = require(script.Parent.Parent.types)
|
local types = require(script.Parent.Parent.types)
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
A hook that subscribes to changes in the given atom or molecule. 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
|
||||||
|
subscription is created once when the component is mounted.
|
||||||
|
|
||||||
|
@param molecule The atom or molecule to subscribe to.
|
||||||
|
@param dependencies An array of values that the subscription depends on.
|
||||||
|
@return The current state.
|
||||||
|
]=]
|
||||||
local function useAtom<State>(molecule: Molecule<State>, dependencies: { any }?): State
|
local function useAtom<State>(molecule: Molecule<State>, dependencies: { any }?): State
|
||||||
local React = requireReact()
|
local React = requireReact()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,25 @@ local store = require(script.Parent.store)
|
||||||
local types = require(script.Parent.types)
|
local types = require(script.Parent.types)
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Subscribes to changes in the given atom or molecule. 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.
|
||||||
|
@return A function that unsubscribes the callback.
|
||||||
|
]=]
|
||||||
local function subscribe<T>(molecule: Molecule<T>, callback: (state: T, prev: T) -> ()): () -> ()
|
local function subscribe<T>(molecule: Molecule<T>, callback: (state: T, prev: T) -> ()): () -> ()
|
||||||
local captured, state = store.capture(molecule)
|
local captured, state = store.capture(molecule)
|
||||||
|
|
||||||
local function listener()
|
local function listener()
|
||||||
local prevState = state
|
local prevState = state
|
||||||
state = molecule()
|
state = molecule()
|
||||||
|
if state ~= prevState then
|
||||||
callback(state, prevState)
|
callback(state, prevState)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
for atom in captured do
|
for atom in captured do
|
||||||
store.listeners[atom][listener] = true
|
store.listeners[atom][listener] = true
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,29 @@ type SyncPayload = types.SyncPayload
|
||||||
local patch = require(script.Parent.patch)
|
local patch = require(script.Parent.patch)
|
||||||
|
|
||||||
type ClientOptions = {
|
type ClientOptions = {
|
||||||
|
--[=[
|
||||||
|
The atoms to synchronize with the server.
|
||||||
|
]=]
|
||||||
atoms: { [string]: Atom<any> },
|
atoms: { [string]: Atom<any> },
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientSyncer = {
|
type ClientSyncer = {
|
||||||
|
--[=[
|
||||||
|
Applies a patch or initializes the state of the atoms with the given
|
||||||
|
payload from the server.
|
||||||
|
|
||||||
|
@param payload The patch or hydration payload to apply.
|
||||||
|
]=]
|
||||||
sync: (self: ClientSyncer, payload: SyncPayload) -> (),
|
sync: (self: ClientSyncer, payload: SyncPayload) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Creates a `ClientSyncer` object that receives patches from the server and
|
||||||
|
applies them to the local state.
|
||||||
|
|
||||||
|
@param options The atoms to synchronize with the server.
|
||||||
|
@return A `ClientSyncer` object.
|
||||||
|
]=]
|
||||||
local function client(options: ClientOptions): ClientSyncer
|
local function client(options: ClientOptions): ClientSyncer
|
||||||
local atoms = options.atoms
|
local atoms = options.atoms
|
||||||
local self = {} :: ClientSyncer
|
local self = {} :: ClientSyncer
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
local client = require(script.client)
|
local client = require(script.client)
|
||||||
local server = require(script.server)
|
local server = require(script.server)
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Synchronizes state between the client and server. The server sends patches
|
||||||
|
to the client, which applies them to its local state.
|
||||||
|
]=]
|
||||||
return {
|
return {
|
||||||
client = client,
|
client = client,
|
||||||
server = server,
|
server = server,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
local validate = require(script.Parent.validate)
|
local validate = require(script.Parent.validate)
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
A special value that denotes the absence of a value. Used to represent
|
||||||
|
undefined values in patches.
|
||||||
|
]=]
|
||||||
local NONE = { __none = "__none" }
|
local NONE = { __none = "__none" }
|
||||||
|
|
||||||
local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,51 @@ local setInterval = require(script.Parent.Parent.utils.setInterval)
|
||||||
local patch = require(script.Parent.patch)
|
local patch = require(script.Parent.patch)
|
||||||
|
|
||||||
type ServerOptions = {
|
type ServerOptions = {
|
||||||
|
--[=[
|
||||||
|
The atoms to synchronize with the client.
|
||||||
|
]=]
|
||||||
atoms: { [string]: Atom<any> },
|
atoms: { [string]: Atom<any> },
|
||||||
|
--[=[
|
||||||
|
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
|
||||||
|
negative value to disable automatic syncing.
|
||||||
|
]=]
|
||||||
interval: number?,
|
interval: number?,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerSyncer = {
|
type ServerSyncer = {
|
||||||
|
--[=[
|
||||||
|
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`
|
||||||
|
is called with the player and the payload to send.
|
||||||
|
|
||||||
|
Note that the `payload` object should not be mutated. If you need to
|
||||||
|
modify the payload, apply the changes to a copy of the object.
|
||||||
|
|
||||||
|
@param callback The function to call when the state changes.
|
||||||
|
@return A cleanup function that unsubscribes all listeners.
|
||||||
|
]=]
|
||||||
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
||||||
|
--[=[
|
||||||
|
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.
|
||||||
|
|
||||||
|
@param player The player to hydrate.
|
||||||
|
]=]
|
||||||
hydrate: (self: ServerSyncer, player: Player) -> (),
|
hydrate: (self: ServerSyncer, player: Player) -> (),
|
||||||
|
--[=[
|
||||||
|
@deprecated For internal use only.
|
||||||
|
]=]
|
||||||
_sendPatch: (self: ServerSyncer, player: Player) -> (),
|
_sendPatch: (self: ServerSyncer, player: Player) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Creates a `ServerSyncer` object that sends patches to the client and
|
||||||
|
hydrates the client's state.
|
||||||
|
|
||||||
|
@param options The atoms to synchronize with the client.
|
||||||
|
@return A `ServerSyncer` object.
|
||||||
|
]=]
|
||||||
local function server(options: ServerOptions): ServerSyncer
|
local function server(options: ServerOptions): ServerSyncer
|
||||||
local atoms = options.atoms
|
local atoms = options.atoms
|
||||||
local interval = options.interval or 0
|
local interval = options.interval or 0
|
||||||
|
|
@ -42,7 +77,7 @@ local function server(options: ServerOptions): ServerSyncer
|
||||||
end)
|
end)
|
||||||
|
|
||||||
state[key] = atom()
|
state[key] = atom()
|
||||||
stateSnapshot[key] = atom()
|
stateSnapshot[key] = state[key]
|
||||||
end
|
end
|
||||||
|
|
||||||
local disconnect = setInterval(function()
|
local disconnect = setInterval(function()
|
||||||
|
|
@ -51,7 +86,7 @@ local function server(options: ServerOptions): ServerSyncer
|
||||||
end
|
end
|
||||||
|
|
||||||
local diffs = patch.diff(stateSnapshot, state)
|
local diffs = patch.diff(stateSnapshot, state)
|
||||||
stateSnapshot = table.clone(state)
|
stateSnapshot = state
|
||||||
changed = false
|
changed = false
|
||||||
|
|
||||||
for _, player in Players:GetPlayers() do
|
for _, player in Players:GetPlayers() do
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ local function isUnsafeTable(object: { [any]: any })
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Validates a value to ensure it can be synced over a remote event.
|
||||||
|
|
||||||
|
@param value The value to validate.
|
||||||
|
@param key The key of the value in the table.
|
||||||
|
@error Throws an error if the value cannot be synced.
|
||||||
|
]=]
|
||||||
local function validate(value: any, key: any)
|
local function validate(value: any, key: any)
|
||||||
local typeOfKey = type(key)
|
local typeOfKey = type(key)
|
||||||
local typeOfValue = type(value)
|
local typeOfValue = type(value)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,38 @@
|
||||||
export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))
|
export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
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> = (() -> T) & (state: T | (T) -> T) -> T
|
export type Atom<T> = (() -> 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 Molecule<T> = () -> T
|
export type Molecule<T> = () -> T
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Optional configuration for creating an atom.
|
||||||
|
]=]
|
||||||
export type AtomOptions<T> = {
|
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,
|
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 = {
|
export type SyncPayload = {
|
||||||
type: "patch" | "init",
|
type: "patch" | "init",
|
||||||
data: { [any]: any },
|
data: { [any]: any },
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
--[=[
|
||||||
|
Trigger a garbage collection cycle by allocating a large amount of junk memory
|
||||||
|
and then waiting for the next frame.
|
||||||
|
]=]
|
||||||
local function collect()
|
local function collect()
|
||||||
for _ = 1, 1e4 do
|
for _ = 1, 1e4 do
|
||||||
local _ = table.create(1e3)
|
local _ = table.create(1e3)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
--[=[
|
||||||
|
Counts the number of elements in a table.
|
||||||
|
|
||||||
|
@param object The table to count.
|
||||||
|
@return The number of elements in the table.
|
||||||
|
]=]
|
||||||
local function count(object: any): number
|
local function count(object: any): number
|
||||||
local count = 0
|
local count = 0
|
||||||
for _ in next, object do
|
for _ in next, object do
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
Schedules a function to be called at a fixed interval of `seconds`.
|
||||||
|
|
||||||
|
@param callback The function to call.
|
||||||
|
@param seconds The interval in seconds.
|
||||||
|
@return A function that cancels the interval.
|
||||||
|
]=]
|
||||||
local function setInterval(callback: () -> (), seconds: number): () -> ()
|
local function setInterval(callback: () -> (), seconds: number): () -> ()
|
||||||
if seconds < 0 then
|
if seconds < 0 then
|
||||||
return function() end
|
return function() end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue