refactor: remove pnpm, fix tests

This commit is contained in:
littensy 2025-05-03 20:30:10 -07:00
parent 861ee5acd7
commit e34b0c9aca
65 changed files with 353 additions and 2458 deletions

View file

@ -1,16 +0,0 @@
{
"name": "@rbxts/charm-sync",
"version": "0.3.0",
"description": "Sync server state with clients using Charm",
"repository": "littensy/charm",
"license": "MIT",
"main": "src/init.luau",
"types": "src/index.d.ts",
"scripts": {
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
"publish:wally": "wally publish"
},
"peerDependencies": {
"@rbxts/charm": "workspace:*"
}
}

View file

@ -1,6 +1,7 @@
local Charm = require(script.Parent.Parent.Charm)
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type AtomMap = types.AtomMap
type SyncPayload = types.SyncPayload

View file

@ -1,4 +1,5 @@
local types = require(script.Parent.types)
type AtomMap = types.AtomMap
type NestedAtomMap = {

View file

@ -1,214 +0,0 @@
import { Atom, Selector } from "@rbxts/charm";
export = CharmSync;
export as namespace CharmSync;
type Cleanup = () => void;
type Key = string | number | symbol;
declare namespace CharmSync {
type AtomMap = Record<string, Atom<any>>;
type SelectorMap = Record<string, Selector<any>>;
/**
* @deprecated Use `SelectorMap` instead.
*/
type MoleculeMap = SelectorMap;
/**
* Infers the type of the return values produced by a map of functions.
*/
type StateOfMap<T> = {
readonly [P in keyof T]: T[P] extends Selector<infer State> ? State : never;
};
/**
* Represents the removal of a value from the state.
*/
interface None {
readonly __none: "__none";
}
type MaybeNone<T> = undefined extends T ? None : never;
/**
* Creates a `ClientSyncer` object that receives patches from the server and
* applies them to the local state.
*
* @client
* @param options The atoms to synchronize with the server.
* @returns A `ClientSyncer` object.
*/
function client<Atoms extends AtomMap>(options: ClientOptions<Atoms>): ClientSyncer<Atoms>;
/**
* Creates a `ServerSyncer` object that sends patches to the client and
* hydrates the client's state.
*
* @server
* @param options The atoms to synchronize with the client.
* @returns A `ServerSyncer` object.
*/
function server<Selectors extends SelectorMap, Serialize extends boolean = true>(
options: ServerOptions<Selectors, Serialize>,
): ServerSyncer<Selectors, Serialize>;
/**
* 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`.
*/
function isNone(value: unknown): value is None;
interface NestedAtomMap {
readonly [key: string]: NestedAtomMap | Atom<any>;
}
type AppendPath<Path extends Key | undefined, Name extends Key> = undefined extends Path
? Name
: Name extends string
? Path extends string
? `${Path}/${Name}`
: never
: never;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type IntersectValues<T> = UnionToIntersection<{ readonly [K in keyof T]: T[K] }[keyof T]>;
type FlattenNestedAtoms<Atoms extends NestedAtomMap, Path extends Key | undefined = undefined> = IntersectValues<{
readonly [Name in keyof Atoms as AppendPath<Path, Name>]: Atoms[Name] extends Atom<any>
? { readonly [K in AppendPath<Path, Name>]: Atoms[Name] }
: Atoms[Name] extends NestedAtomMap
? FlattenNestedAtoms<Atoms[Name], AppendPath<Path, Name>>
: never;
}>;
/**
* Flattens a nested atom map into a single object with slash-separated
* keys. Useful for recursively collecting atoms returned by modules.
*
* @param atoms The nested atom map to flatten.
* @return A flattened atom map.
*/
function flatten<Atoms extends NestedAtomMap>(atoms: Atoms): FlattenNestedAtoms<Atoms>;
type DataTypes = {
[P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P];
};
/**
* A type that should not be made partial in patches.
*/
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 `sync.isNone` function.
*/
type SyncPatch<State, Serialize extends boolean = true> = MaybeNone<State> | State extends
| ReadonlyMap<infer K, infer V>
| Map<infer K, infer V>
? ReadonlyMap<K, SyncPatch<V, Serialize> | None>
: State extends Set<infer T> | ReadonlySet<infer T>
? ReadonlyMap<T, true | None>
: State extends readonly (infer T)[]
? Serialize extends true
? ReadonlyMap<string | number, SyncPatch<T, Serialize> | None>
: readonly (SyncPatch<T, Serialize> | None | undefined)[]
: State extends DataType
? State
: State extends object
? { readonly [P in keyof State]?: SyncPatch<State[P], Serialize> }
: State;
/**
* A payload that can be sent from the server to the client to synchronize
* state between the two.
*/
type SyncPayload<Selectors extends SelectorMap, Serialize extends boolean = true> =
| { readonly type: "init"; readonly data: StateOfMap<Selectors> }
| { readonly type: "patch"; readonly data: SyncPatch<StateOfMap<Selectors>, Serialize> };
interface ClientOptions<Atoms extends AtomMap> {
/**
* The atoms to synchronize with the server.
*/
atoms: Atoms;
/**
* Whether to ignore patches sent before the client has been hydrated.
* Defaults to `true`.
*/
ignoreUnhydrated?: boolean;
}
interface ServerOptions<Selectors extends SelectorMap, Serialize extends boolean = true> {
/**
* The atoms to synchronize with the client.
*/
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
* negative value to disable automatic syncing.
*/
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`.
*
* If `true`, the broadcaster will send a list of payloads to the client
* instead of a single payload. The client will apply each payload in order
* to reconstruct the state's changes over time.
*/
preserveHistory?: boolean;
/**
* When `true`, Charm will apply validation and serialize unsafe arrays
* to address remote event argument limitations. Defaults to `true`.
*
* This option should be disabled if your network library uses a custom
* serialization method (i.e. Zap, ByteNet) to prevent interference.
*/
autoSerialize?: Serialize;
}
interface ClientSyncer<Atoms extends AtomMap> {
/**
* Applies a patch or initializes the state of the atoms with the given
* payload from the server.
*
* @param payloads The patches or hydration payloads to apply.
*/
sync(...payloads: SyncPayload<Atoms, boolean>[]): void;
}
interface ServerSyncer<Selectors extends SelectorMap, Serialize extends boolean> {
/**
* 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 payloads to send.
*
* Note that a `payload` object should not be mutated. If you need to modify
* a 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, ...payloads: SyncPayload<Selectors, Serialize>[]) => 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;
}
}

View file

@ -3,6 +3,7 @@ local flatten = require(script.flatten)
local patch = require(script.patch)
local server = require(script.server)
local types = require(script.types)
local validate = require(script.validate)
export type SyncPayload = types.SyncPayload
@ -14,5 +15,7 @@ return {
client = client,
server = server,
flatten = flatten,
validate = validate,
patch = patch,
isNone = patch.isNone,
}

View file

@ -4,6 +4,7 @@ local Charm = require(script.Parent.Parent.Charm)
local interval = require(script.Parent.interval)
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type AtomMap = types.AtomMap
type SyncPayload = types.SyncPayload

View file

@ -1,4 +1,5 @@
local Charm = require(script.Parent.Parent.Charm)
type Atom<T> = Charm.Atom<T>
--[=[

View file

@ -5,4 +5,4 @@ registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]
charm = "littensy/charm@^0.10"
Charm = "littensy/charm@^0.10"

View file

@ -1,13 +0,0 @@
{
"name": "@rbxts/charm",
"version": "0.10.0",
"description": "An atomic state management library for Roblox",
"repository": "littensy/charm",
"license": "MIT",
"main": "src/init.luau",
"types": "src/index.d.ts",
"scripts": {
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
"publish:wally": "wally publish"
}
}

View file

@ -1,165 +0,0 @@
export = Charm;
export as namespace Charm;
type Dispose = () => void;
declare namespace Charm {
/**
* A primitive state container that can be read from or written to. When
* the state changes, all subscribers are notified.
*
* @param state The next state or a function that produces the next state.
* @returns The current state.
*/
interface Atom<State> {
(): State;
(nextState: State | ((prevState: State) => State)): State;
}
/**
* 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 Selector<State> = () => State;
/**
* Infers the type of the state produced by the given function.
*/
type StateOf<T> = T extends () => infer State ? State : never;
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;
}
/**
* Creates a new atom with the given state.
*
* @param state The initial state.
* @param options Optional configuration.
* @returns A new atom.
*/
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
// Overload for no arguments
function atom<State>(state?: State, options?: AtomOptions<State>): Atom<State | undefined>;
/**
* 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.
*
* @param callback The function that produces the state.
* @returns A new read-only atom.
*/
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 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: () => 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
* changes. Returns a cleanup function that unsubscribes the callback.
*
* @param callback The function to run.
* @returns A function that unsubscribes the callback.
*/
function effect(callback: (dispose: Dispose) => Dispose | void): Dispose;
/**
* 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 batched(callback: () => void): void;
/**
* Returns the result of the function without subscribing to changes.
*
* @param callback The atom or selector to get the state of.
* @param args Arguments to pass to the function.
* @returns The current state.
*/
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
* 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.
* @returns A function that unsubscribes all instances.
*/
function observe<Item>(
callback: () => readonly Item[],
factory: (item: Item, index: number) => Dispose | void,
): Dispose;
function observe<Key, Item>(
callback: () => Map<Key, Item> | ReadonlyMap<Key, Item>,
factory: (item: Item, key: Key) => Dispose | void,
): Dispose;
function observe<Key extends string | number | symbol, Item>(
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` 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: () => readonly V0[],
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
): () => ReadonlyMap<K1, V1>;
function mapped<V0, V1>(
callback: () => readonly V0[],
mapper: (value: V0, index: number) => V1,
): () => readonly V1[];
function mapped<K0, V0, K1 = K0, V1 = V0>(
callback: () => Map<K0, V0> | ReadonlyMap<K0, V0>,
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
): () => ReadonlyMap<K1, V1>;
function mapped<K0 extends string | number | symbol, V0, K1 = K0, V1 = V0>(
callback: () => { readonly [P in K0]: V0 },
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
): () => ReadonlyMap<K1, V1>;
}

View file

@ -6,6 +6,7 @@ local effectScope = require("./effectScope")
local listen = require("./listen")
local mapped = require("./mapped")
local observe = require("./observe")
local signals = require("./signals")
local subscribe = require("./subscribe")
local types = require("./types")
local untracked = require("./untracked")
@ -25,4 +26,5 @@ return {
observe = observe,
subscribe = subscribe,
untracked = untracked,
signals = signals,
}

View file

@ -19,7 +19,7 @@ local useState = React.useState
@param dependencies An array of values that the subscription depends on.
@return The current state.
]=]
local function useAtom<State>(callback: Charm.Selector<State>, dependencies: { any }?): State
local function useAtom<State>(callback: () -> State, dependencies: { any }?): State
local state, setState = useState(callback)
useEffect(function()

View file

@ -5,5 +5,5 @@ registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]
charm = "littensy/charm@^0.10"
Charm = "littensy/charm@^0.10"
React = "jsdotlua/react@^17"

View file

@ -1,17 +0,0 @@
{
"name": "@rbxts/react-charm",
"version": "0.3.0",
"description": "React bindings for Charm",
"repository": "littensy/charm",
"license": "MIT",
"main": "src/init.luau",
"types": "src/index.d.ts",
"scripts": {
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
"publish:wally": "wally publish"
},
"peerDependencies": {
"@rbxts/charm": "workspace:*",
"@rbxts/react": "*"
}
}

View file

@ -1,13 +0,0 @@
/**
* 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
* selector is re-created whenever the dependencies change. Otherwise, the
* subscription is created once when the component is mounted.
*
* @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>(callback: () => State, dependencies?: unknown[]): State;

View file

@ -5,5 +5,5 @@ registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]
charm = "littensy/charm@^0.10"
Charm = "littensy/charm@^0.10"
Vide = "centau/vide@^0.3"

View file

@ -1,17 +0,0 @@
{
"name": "@rbxts/vide-charm",
"version": "0.3.0",
"description": "Vide bindings for Charm",
"repository": "littensy/charm",
"license": "MIT",
"main": "src/init.luau",
"types": "src/index.d.ts",
"scripts": {
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
"publish:wally": "wally publish"
},
"peerDependencies": {
"@rbxts/charm": "workspace:*",
"@rbxts/vide": "*"
}
}

View file

@ -1,7 +0,0 @@
/**
* 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: () => T): () => T;