mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:44:08 +00:00
* feat: use alien-signals * refactor: remove pnpm, fix tests * fix: `effectScope` should set scope * fix: check previous value before calling listener * refactor: improve imports * refactor: exclude re-exports from wally * refactor: embrace scope cleanup, restructure code * fix: update pkgs dir references * ci: simplify pesde install * build: install dev deps with wally * feat: add detached, add more tests * test: simplify mapped test update * chore: rephrase doc comment * perf: optimize subscribe and mapped * refactor: avoid ambiguous optional function type * refactor: add assertion error messages * perf: optimize mapped, add benches * chore: delete redundant benchmarks * perf: simplify mapped logic * test: prettify test library * test: try different gray * test: color brackets * refactor: sync 2.0.4 * refactor: use setCurrentSub instead of pauseTracking * test: improve test suite * test: separate parent from root * refactor: iterate over previous data in mapped * refactor: improve logic * style: use `---` doc comments * refactor: improve logic * chore: housekeeping * ci: add pnpm step * ci: init submodules * test: use lune * refactor: improve structure and alien-signals integration * ci: update test file path * feat: add peek and roblox-ts types * feat: add deep freeze flag, misc refactors * refactor: reduce type casting, housekeeping * refactor: rewrite untracked, update alien-signals * chore: update eslint, housekeeping * refactor: start charm-sync rewrite * fix: send init payload even if atom did not update * test: add all sync tests * feat: prepare for alpha release * feat: add peek, make untracked escape scope * fix: wrap hooks in untracked, update types * feat: add flag for inner effect tracking * refactor: add docs, remove some modules * refactor: align with alien-signals 3.0 * feat: add signal api * refactor: update charm sync api * feat: update alien-signals * refactor: remove unnecessary features, update docs * feat: finalize sync docs * docs: update sync section * docs: simplify examples * docs: capitalize headers * docs: add migration tips * docs: fix whitespace * docs: rephrase computed arg change * refactor: rename batch, misc fixes & perf updates * refactor: simplify charm sync validation * feat: update alien-signals, add recursion flag * feat: add `recursive()` * fix: allow dispose in observers * feat: add `trigger()`, refactor source * docs: update readme * chore: prepare release candidate * test: make some tests more reliable * docs: fix typo * feat: add `reactive()` * chore: bump versions * docs: reword reactive docs * docs: fix typo * feat: add `toRaw()` * fix: check for nil diffs * chore: bump versions * perf: do not create proxies for iteration * fix: make `toRaw` track all calls * fix: check none before patching mutable tables * chore: bump charm-sync version * docs: add fishing game example project * feat: sync signals that return reactive proxy * fix: refine reactive proxy logic * fix: prevent early reactive gc, alien-signals 3.1.1 * fix: wrap iterated values in `reactive()` * fix: loosen client signal types * chore: bump release candidate * fix: remove unsynced signals immediately * test: use lute for tests * chore: bump versions * ci: set up lute * refactor: improve source readability * ci: upgrade lute * build: loosen workspace version requirements * chore: bump deps * fix: update signal overload for setter (#58) * fix: add missing recursed check * test: add important vue reactivity tests * chore: fix react tests, run stylua * chore: update vide * docs: improve intro and explain getters * docs: adjust ordering by importance * docs: move listen nesting note * build: fix react peer dep for ci * chore: update lockfile * ci: use javascript eslint config * ci: ignore eslint config file * refactor: move test directories * feat: allow recursion in effects by default * release: charm v0.11.0-rc.4 * refactor: remove recursive() * refactor: reorganize repo, create deep-charm * docs: rephrase some sections * feat: export getter and setter types * docs: rephrase intro again * feat!: run listen and subscribe callback in untracked * refactor!: remove peek * docs: note peek in migration guide * release: charm v0.11.0-rc.5 * release: charm-sync v0.4.0-rc.7 * release: react-charm v0.4.0-rc.4 * fix: remove old keys when updating key in `mapped()` * refactor: shorten error traceback, housekeeping * refactor: keep error level in some cases * fix: check if cleanup functions disposed effect * refactor!: run effects in batched * ci: check eslint and prettier * perf: run observe fn in untracked * perf: call mapper as-is in `mapped()` * refactor: error for effect cleanup errors * refactor: use new type solver * ci: disable new solver flag until 0.714 * chore: housekeeping * refactor: improve errors in nonstrict, optimize noyield * chore: housekeeping * refactor: satisfy new solver * refactor: improve error traceback * docs: link roblox signals * chore: housekeeping * perf: remove `clearPending` inline operation * refactor: check type of cleanup * ci: remove selene * fix: always return pending value in signal setter * test: add multi benchmark * release: charm v0.11.0-rc.6 * test: add cleanup order tests * chore: bump deps * docs: add warning for deep-charm and note for `observe()` --------- Co-authored-by: safazi <safazics@gmail.com>
292 lines
9.6 KiB
TypeScript
292 lines
9.6 KiB
TypeScript
import { Atom, Setter } from "@rbxts/charm";
|
|
|
|
type Key = string | number | symbol;
|
|
|
|
/**
|
|
* Represents the removal of a value from the state.
|
|
*/
|
|
export interface None {
|
|
readonly __none: "__none";
|
|
}
|
|
|
|
export type MaybeNone<T> = undefined extends T ? None : never;
|
|
|
|
type ReactiveObject = { [key: Key]: any };
|
|
|
|
type ServerSignalMap = Record<string, (() => any) | ReactiveObject>;
|
|
|
|
type ClientSignalMap = Record<string, Setter<any> | ReactiveObject>;
|
|
|
|
/**
|
|
* Infers the type of the return values produced by a map of functions.
|
|
*/
|
|
export type StateOfMap<T> = {
|
|
readonly [P in keyof T]: T[P] extends () => infer State ? State : never;
|
|
};
|
|
|
|
type DataTypes = {
|
|
[P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P];
|
|
};
|
|
|
|
type DataType = DataTypes[keyof DataTypes];
|
|
|
|
/**
|
|
* A partial patch that can be applied to the state to update it. Represents
|
|
* the difference between the current state and the next state.
|
|
*
|
|
* If a value was removed, it is replaced with `None`. This can be checked
|
|
* using the `isNone` function.
|
|
*/
|
|
// TODO: Optimize type, this looks overly complex
|
|
export type SyncPatch<State, FixArrays extends boolean = true> = MaybeNone<State> | State extends
|
|
| ReadonlyMap<infer K, infer V>
|
|
| Map<infer K, infer V>
|
|
? ReadonlyMap<K, SyncPatch<V, FixArrays> | None>
|
|
: State extends Set<infer T> | ReadonlySet<infer T>
|
|
? ReadonlyMap<T, true | None>
|
|
: State extends readonly (infer T)[]
|
|
? FixArrays extends true
|
|
? ReadonlyMap<string | number, SyncPatch<T, FixArrays> | None>
|
|
: readonly (SyncPatch<T, FixArrays> | None | undefined)[]
|
|
: State extends DataType
|
|
? State
|
|
: State extends object
|
|
? { readonly [P in keyof State]?: SyncPatch<State[P], FixArrays> }
|
|
: State;
|
|
|
|
/**
|
|
* A payload that can be sent from the server to the client to synchronize
|
|
* state between the two.
|
|
*/
|
|
export type SyncPayload<Getters extends ServerSignalMap = ServerSignalMap, FixArrays extends boolean = true> =
|
|
| { type: "init"; data: StateOfMap<Getters> }
|
|
| { type: "patch"; data: SyncPatch<StateOfMap<Getters>, FixArrays> };
|
|
|
|
/**
|
|
* Global configuration options that affect the behavior of Charm Sync.
|
|
*/
|
|
export const config: {
|
|
/**
|
|
* 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.
|
|
*
|
|
* @server
|
|
*/
|
|
interval: number;
|
|
/**
|
|
* Whether the history of state changes since the client's last update
|
|
* should be preserved. This is useful for values that change multiple
|
|
* times per frame, where each individual change is important. Defaults
|
|
* to `false` for performance reasons.
|
|
*
|
|
* @server
|
|
*/
|
|
preserveHistory: boolean;
|
|
/**
|
|
* When `true`, Charm will modify sparse arrays to prevent data loss from
|
|
* remote event limitations. When an unsafe array is detected, numeric
|
|
* indices will be converted to string keys so that the array can be sent
|
|
* safely and then converted back to an array on the receiving end.
|
|
*
|
|
* This option should be disabled if your network library uses a custom
|
|
* serialization method (i.e. Zap, ByteNet) to prevent interference.
|
|
*
|
|
* @server
|
|
*/
|
|
fixArrays: boolean;
|
|
/**
|
|
* When `true`, synced state containing unsafe sparse arrays or mixed
|
|
* tables will emit a warning. Only checked in strict mode and if
|
|
* `fixArrays` is enabled. Defaults to `true`.
|
|
*
|
|
* @server
|
|
*/
|
|
validatePatches: boolean;
|
|
};
|
|
|
|
/**
|
|
* @client
|
|
*/
|
|
export namespace client {
|
|
/**
|
|
* Registers a map of setters to the state with the corresponding keys on the
|
|
* server. When the server sends updates, these setters will be called with
|
|
* the new values.
|
|
*
|
|
* Atoms, signals converted to atoms, and signal setter functions are allowed.
|
|
* Note that `server.addSignalsToClient` still requires some way to get the
|
|
* state.
|
|
*
|
|
* @param signals A map of setter functions to sync with the server.
|
|
*/
|
|
export function addSignals<Signals extends ClientSignalMap = ClientSignalMap>(signals: Signals): void;
|
|
|
|
/**
|
|
* Unregisters from server state updates for the given keys. The signals
|
|
* will retain their current values, but will no longer receive updates
|
|
* from the server.
|
|
*/
|
|
export function removeSignals<Signals extends ServerSignalMap | ClientSignalMap = ClientSignalMap>(
|
|
...keys: (keyof Signals)[]
|
|
): void;
|
|
|
|
/**
|
|
* Unsubscribes from all server state updates.
|
|
*/
|
|
export function removeAllSignals(): void;
|
|
|
|
/**
|
|
* Merges the server's state patches into the client's state, either
|
|
* initializing it with the full state or merging a partial patch.
|
|
*
|
|
* @param payloads The state updates received from the server.
|
|
*/
|
|
export function patch<Getters extends ServerSignalMap = ServerSignalMap, FixArrays extends boolean = true>(
|
|
payloads: SyncPayload<Getters, FixArrays>[],
|
|
): void;
|
|
}
|
|
|
|
/**
|
|
* @server
|
|
*/
|
|
export namespace server {
|
|
/**
|
|
* Subscribes a client to the given signals. When an update occurs, the client
|
|
* will receive a partial state patch to merge into their local state. May be
|
|
* called multiple times to subscribe to additional keys.
|
|
*
|
|
* Atoms, computed signals, and signal getter functions are allowed. Note
|
|
* that `client.addSignals` still requires some way to set the state.
|
|
*
|
|
* @param client The client receiving state updates.
|
|
* @param signals A map of getter functions to sync with the client.
|
|
*/
|
|
export function addSignalsToClient<Signals extends ServerSignalMap = ServerSignalMap>(
|
|
client: Player,
|
|
signals: Signals,
|
|
): void;
|
|
|
|
/**
|
|
* Unsubscribes a client from receiving all state updates. To only unsubscribe
|
|
* from specific keys, use `removeSignalsFromClient` instead.
|
|
*
|
|
* @param client The client receiving state updates.
|
|
*/
|
|
export function removeClient(client: Player): void;
|
|
|
|
/**
|
|
* Unsubscribes a client from receiving updates for the given keys.
|
|
*
|
|
* @param client The client receiving state updates.
|
|
* @param keys The keys of the state to unsubscribe from.
|
|
*/
|
|
export function removeSignalsFromClient<Signals extends ServerSignalMap = ServerSignalMap>(
|
|
client: Player,
|
|
...keys: (keyof Signals)[]
|
|
): void;
|
|
|
|
/**
|
|
* Uses the callback to send state updates to clients that are subscribed
|
|
* to state changes. Starts automatically sending updates at the interval
|
|
* specified in `config.interval`.
|
|
*
|
|
* @param onSync Called when sending patches to a client.
|
|
*/
|
|
export function connect<Getters extends ServerSignalMap = ServerSignalMap, FixArrays extends boolean = true>(
|
|
onSync: (client: Player, payloads: SyncPayload<Getters, FixArrays>[]) => void,
|
|
): void;
|
|
|
|
/**
|
|
* Stops syncing state updates to clients at the automatic interval.
|
|
* Flushing can still be performed manually via `flush`.
|
|
*/
|
|
export function disconnect(): void;
|
|
|
|
/**
|
|
* Immediately sends pending updates to all clients. Normally, updates are
|
|
* sent at the interval specified in `config.interval`, but this function
|
|
* can be used to force an immediate sync.
|
|
*/
|
|
export function flush(): void;
|
|
|
|
/**
|
|
* Unsubscribes all clients, stops tracking all state, and disconnects
|
|
* from the automatic sync interval.
|
|
*/
|
|
export function reset(): void;
|
|
}
|
|
|
|
export namespace patch {
|
|
/**
|
|
* Converts `nil` values to the special `None` symbol. This is useful when
|
|
* preparing data for serialization, since `nil` can represent both the
|
|
* absence of a value and the removal of a value in patches.
|
|
*/
|
|
export function nilToNone<T>(value: T | undefined): T | None;
|
|
|
|
/**
|
|
* Compares two states and returns a patch that can be applied to the
|
|
* current state to produce the next state.
|
|
*
|
|
* @param currentState The current state.
|
|
* @param nextState The next state.
|
|
* @returns A patch that can be applied to the current state.
|
|
*/
|
|
export function diff<State, FixArrays extends boolean = true>(
|
|
currentState: State,
|
|
nextState: State,
|
|
): SyncPatch<State, FixArrays>;
|
|
|
|
/**
|
|
* Applies a patch to a state and returns the resulting value.
|
|
*
|
|
* @param currentState The current state.
|
|
* @param statePatch The patches to apply.
|
|
* @returns The new state with the patch applied.
|
|
*/
|
|
export function applyImmutable<State, FixArrays extends boolean = true>(
|
|
currentState: State,
|
|
statePatch: SyncPatch<State, FixArrays>,
|
|
): State;
|
|
}
|
|
|
|
type DeepFlattenImpl<T, Prefix extends string = ""> = {
|
|
[K in keyof T]: T[K] extends DataType | Callback
|
|
? { [P in `${Prefix}${Exclude<K, symbol>}`]: T[K] }
|
|
: T[K] extends object
|
|
? DeepFlattenImpl<T[K], `${Prefix}${Exclude<K, symbol>}/`>
|
|
: { [P in `${Prefix}${Exclude<K, symbol>}`]: T[K] };
|
|
}[keyof T];
|
|
|
|
type DeepFlatten<T> = Reconstruct<UnionToIntersection<DeepFlattenImpl<T>>>;
|
|
|
|
/**
|
|
* Flattens a nested table into a single table with slash-separated keys.
|
|
* Useful for recursively collecting atoms returned by modules.
|
|
*
|
|
* @param input A nested table.
|
|
* @return A flattened table with slash-separated keys.
|
|
*/
|
|
export function flatten<T>(input: T): DeepFlatten<T>;
|
|
|
|
/**
|
|
* Checks whether a value is `None`. If `true`, the value is scheduled to be
|
|
* removed from the state when the patch is applied.
|
|
*
|
|
* @param value The value to check.
|
|
* @returns `true` if the value is `None`, otherwise `false`.
|
|
*/
|
|
export function isNone(value: unknown): value is None;
|
|
|
|
/**
|
|
* Converts a signal getter and setter into an atom. This is useful for
|
|
* syncing client signals with the server, as the sync system requires both
|
|
* the getter and setter to update state.
|
|
*
|
|
* @param getter A function that returns the current value of the signal.
|
|
* @param setter A function that sets the value of the signal.
|
|
* @returns An atom that can either get or set the signal's value.
|
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#signaltoatomgetter-setter
|
|
*/
|
|
export function signalToAtom<T>(getter: () => T, setter: (value: ((prev: T) => T) | T) => T): Atom<T>;
|