mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 21:01:38 +00:00
refactor: add docs, remove some modules
This commit is contained in:
parent
027c5bf8f8
commit
7a63993293
19 changed files with 558 additions and 180 deletions
|
|
@ -6,6 +6,10 @@ type SyncPayload = types.SyncPayload
|
||||||
|
|
||||||
local atomRegister: { [string]: Charm.Atom } = {}
|
local atomRegister: { [string]: Charm.Atom } = {}
|
||||||
|
|
||||||
|
--- Registers atoms to be kept in sync with the server. Atoms with matching
|
||||||
|
--- keys on the server will be synced to these atoms.
|
||||||
|
---
|
||||||
|
--- @param atoms An object mapping string keys to atoms to be synced.
|
||||||
local function syncAtoms(atoms: { [string]: Charm.Atom })
|
local function syncAtoms(atoms: { [string]: Charm.Atom })
|
||||||
for key, atom in atoms do
|
for key, atom in atoms do
|
||||||
if atomRegister[key] and atomRegister[key] ~= atom then
|
if atomRegister[key] and atomRegister[key] ~= atom then
|
||||||
|
|
@ -17,6 +21,11 @@ local function syncAtoms(atoms: { [string]: Charm.Atom })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Unregisters one or more atoms from being kept in sync with the server.
|
||||||
|
--- The atoms will retain their current values, but will no longer receive
|
||||||
|
--- updates from the server.
|
||||||
|
---
|
||||||
|
--- @param keys The keys of the atoms to stop syncing.
|
||||||
local function unsyncAtoms(...: string)
|
local function unsyncAtoms(...: string)
|
||||||
for index = 1, select("#", ...) do
|
for index = 1, select("#", ...) do
|
||||||
local key = select(index, ...)
|
local key = select(index, ...)
|
||||||
|
|
@ -24,6 +33,9 @@ local function unsyncAtoms(...: string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Unregisters all atoms from being kept in sync with the server. The
|
||||||
|
--- atoms will retain their current values, but will no longer receive
|
||||||
|
--- updates from the server.
|
||||||
local function reset()
|
local function reset()
|
||||||
table.clear(atomRegister)
|
table.clear(atomRegister)
|
||||||
end
|
end
|
||||||
|
|
@ -51,6 +63,11 @@ local function applyPatchPayload(data: { [string | number]: any })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Applies one or more payloads received from the server to the synced
|
||||||
|
--- atoms. Payloads are state patches or initial state updates sent by the
|
||||||
|
--- server, and are usually received via a remote event.
|
||||||
|
---
|
||||||
|
--- @param payloads The payloads to apply.
|
||||||
local function applyPayloads(payloads: { SyncPayload })
|
local function applyPayloads(payloads: { SyncPayload })
|
||||||
for _, payload in payloads do
|
for _, payload in payloads do
|
||||||
if payload.type == "init" then
|
if payload.type == "init" then
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ type NestedAtomMap = {
|
||||||
[string]: NestedAtomMap | () -> any,
|
[string]: NestedAtomMap | () -> any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- 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.
|
||||||
local function flatten(atoms: NestedAtomMap): { [string]: () -> any }
|
local function flatten(atoms: NestedAtomMap): { [string]: () -> any }
|
||||||
local result: { [string]: () -> any } = {}
|
local result: { [string]: () -> any } = {}
|
||||||
|
|
||||||
|
|
|
||||||
128
packages/charm-sync/src/index.d.ts
vendored
128
packages/charm-sync/src/index.d.ts
vendored
|
|
@ -26,9 +26,6 @@ type DataTypes = {
|
||||||
[P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P];
|
[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];
|
type DataType = DataTypes[keyof DataTypes];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,14 +60,8 @@ export type SyncPayload<Selectors extends SelectorMap = SelectorMap, Serialize e
|
||||||
| { type: "patch"; data: SyncPatch<StateOfMap<Selectors>, Serialize> };
|
| { type: "patch"; data: SyncPatch<StateOfMap<Selectors>, Serialize> };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a value is `None`. If `true`, the value is scheduled to be
|
* Global configuration options that affect the behavior of Charm Sync.
|
||||||
* 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;
|
|
||||||
|
|
||||||
export const config: {
|
export const config: {
|
||||||
/**
|
/**
|
||||||
* The interval at which to send patches to the client, in seconds.
|
* The interval at which to send patches to the client, in seconds.
|
||||||
|
|
@ -108,49 +99,142 @@ export const config: {
|
||||||
validatePatches: boolean;
|
validatePatches: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @client
|
||||||
|
*/
|
||||||
export namespace client {
|
export namespace client {
|
||||||
|
/**
|
||||||
|
* Registers atoms to be kept in sync with the server. Atoms with matching
|
||||||
|
* keys on the server will be synced to these atoms.
|
||||||
|
*
|
||||||
|
* @param atoms An object mapping string keys to atoms to be synced.
|
||||||
|
*/
|
||||||
export function syncAtoms(atoms: { [key: string]: Atom<any> }): void;
|
export function syncAtoms(atoms: { [key: string]: Atom<any> }): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters one or more atoms from being kept in sync with the server.
|
||||||
|
* The atoms will retain their current values, but will no longer receive
|
||||||
|
* updates from the server.
|
||||||
|
*
|
||||||
|
* @param keys The keys of the atoms to stop syncing.
|
||||||
|
*/
|
||||||
export function unsyncAtoms(...keys: string[]): void;
|
export function unsyncAtoms(...keys: string[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters all atoms from being kept in sync with the server. The
|
||||||
|
* atoms will retain their current values, but will no longer receive
|
||||||
|
* updates from the server.
|
||||||
|
*/
|
||||||
export function reset(): void;
|
export function reset(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies one or more payloads received from the server to the synced
|
||||||
|
* atoms. Payloads are state patches or initial state updates sent by the
|
||||||
|
* server, and are usually received via a remote event.
|
||||||
|
*
|
||||||
|
* @param payloads The payloads to apply.
|
||||||
|
*/
|
||||||
export function applyPayloads<Atoms extends { [key: string]: Atom<any> }, Serialize extends boolean = true>(
|
export function applyPayloads<Atoms extends { [key: string]: Atom<any> }, Serialize extends boolean = true>(
|
||||||
payloads: SyncPayload<Atoms, Serialize>[],
|
payloads: SyncPayload<Atoms, Serialize>[],
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @server
|
||||||
|
*/
|
||||||
export namespace server {
|
export namespace server {
|
||||||
|
/**
|
||||||
|
* Registers atoms to be kept in sync with a specific client. Should be
|
||||||
|
* called when a player joins the game to start syncing atoms to them. Call
|
||||||
|
* `unsyncAtomsFromClient` or `unsyncClient` to stop syncing.
|
||||||
|
*
|
||||||
|
* Each atom should be assigned a unique key, allowing the server to re-use
|
||||||
|
* patches between multiple clients.
|
||||||
|
*
|
||||||
|
* @param client The player to sync atoms to.
|
||||||
|
* @param atoms An object mapping string keys to atoms to be synced.
|
||||||
|
*/
|
||||||
export function syncAtomsToClient(client: Player, atoms: { [key: string]: Atom<any> }): void;
|
export function syncAtomsToClient(client: Player, atoms: { [key: string]: Atom<any> }): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters one or more atoms from being kept in sync with a specific
|
||||||
|
* client.
|
||||||
|
*
|
||||||
|
* @param client The player to stop syncing atoms to.
|
||||||
|
* @param keys The keys of the atoms to stop syncing.
|
||||||
|
*/
|
||||||
export function unsyncAtomsFromClient(client: Player, ...keys: string[]): void;
|
export function unsyncAtomsFromClient(client: Player, ...keys: string[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters all atoms from being kept in sync with a specific client.
|
||||||
|
* Should be called when a player leaves the game.
|
||||||
|
*
|
||||||
|
* @param client The player to stop syncing atoms to.
|
||||||
|
*/
|
||||||
export function unsyncClient(client: Player): void;
|
export function unsyncClient(client: Player): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts automatically syncing registered atoms to clients. Patches will
|
||||||
|
* be sent to clients whenever the atoms are updated.
|
||||||
|
*
|
||||||
|
* @param onSync The function to call when patches can be sent to a client.
|
||||||
|
* @returns `stopSync()`
|
||||||
|
*/
|
||||||
export function startSync<Atoms extends { [key: string]: Atom<any> }, Serialize extends boolean = true>(
|
export function startSync<Atoms extends { [key: string]: Atom<any> }, Serialize extends boolean = true>(
|
||||||
onSync: (client: Player, payloads: SyncPayload<Atoms, Serialize>[]) => void,
|
onSync: (client: Player, payloads: SyncPayload<Atoms, Serialize>[]) => void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops automatically syncing registered atoms to clients. Patches will
|
||||||
|
* no longer be sent to clients until `startSync` is called again.
|
||||||
|
*/
|
||||||
export function stopSync(): void;
|
export function stopSync(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately sends pending updates and initial states to all clients.
|
||||||
|
* Normally, updates are sent at the interval specified in `config.interval`.
|
||||||
|
* Calling this function forces all pending updates to be sent immediately.
|
||||||
|
*/
|
||||||
export function flush(): void;
|
export function flush(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters all atoms from being kept in sync with all clients, and
|
||||||
|
* stops automatic syncing.
|
||||||
|
*/
|
||||||
export function reset(): void;
|
export function reset(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace patch {
|
export namespace patch {
|
||||||
export function isNone(value: unknown): value is None;
|
/**
|
||||||
|
* 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;
|
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, Serialize extends boolean = true>(
|
export function diff<State, Serialize extends boolean = true>(
|
||||||
currentState: State,
|
currentState: State,
|
||||||
nextState: State,
|
nextState: State,
|
||||||
): SyncPatch<State, Serialize>;
|
): SyncPatch<State, Serialize>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 apply<State, Serialize extends boolean = true>(
|
export function apply<State, Serialize extends boolean = true>(
|
||||||
state: State,
|
currentState: State,
|
||||||
patch: SyncPatch<State, Serialize>,
|
statePatch: SyncPatch<State, Serialize>,
|
||||||
): State;
|
): State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +254,10 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||||
|
|
||||||
type IntersectValues<T> = UnionToIntersection<{ readonly [K in keyof T]: T[K] }[keyof T]>;
|
type IntersectValues<T> = UnionToIntersection<{ readonly [K in keyof T]: T[K] }[keyof T]>;
|
||||||
|
|
||||||
type FlattenNestedAtoms<Atoms extends NestedAtomMap, Path extends Key | undefined = undefined> = IntersectValues<{
|
export 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 [Name in keyof Atoms as AppendPath<Path, Name>]: Atoms[Name] extends Atom<any>
|
||||||
? { readonly [K in AppendPath<Path, Name>]: Atoms[Name] }
|
? { readonly [K in AppendPath<Path, Name>]: Atoms[Name] }
|
||||||
: Atoms[Name] extends NestedAtomMap
|
: Atoms[Name] extends NestedAtomMap
|
||||||
|
|
@ -183,6 +270,15 @@ type FlattenNestedAtoms<Atoms extends NestedAtomMap, Path extends Key | undefine
|
||||||
* Useful for recursively collecting atoms returned by modules.
|
* Useful for recursively collecting atoms returned by modules.
|
||||||
*
|
*
|
||||||
* @param atoms The nested atom map to flatten.
|
* @param atoms The nested atom map to flatten.
|
||||||
* @return A flattened atom map.
|
* @returns A flattened atom map.
|
||||||
*/
|
*/
|
||||||
export function flatten<Atoms extends NestedAtomMap>(atoms: Atoms): FlattenNestedAtoms<Atoms>;
|
export function flatten<Atoms extends NestedAtomMap>(atoms: Atoms): FlattenNestedAtoms<Atoms>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ export type None = types.None
|
||||||
--- deleted values in patches.
|
--- deleted values in patches.
|
||||||
local None: None = { __none = "__none" }
|
local None: None = { __none = "__none" }
|
||||||
|
|
||||||
--- Checks if a value is the special `None` symbol.
|
--- 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.
|
||||||
|
--- @return `true` if the value is `None`, otherwise `false`.
|
||||||
local function isNone(value: any): boolean
|
local function isNone(value: any): boolean
|
||||||
return type(value) == "table" and value.__none == "__none"
|
return type(value) == "table" and value.__none == "__none"
|
||||||
end
|
end
|
||||||
|
|
@ -48,30 +52,30 @@ local function stringifySparseArray(object: { any }): { [string | number]: any }
|
||||||
return dictionary
|
return dictionary
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Compares two states and returns a patch that can be applied to the previous
|
--- Compares two states and returns a patch that can be applied to the current
|
||||||
--- state to produce the next state.
|
--- state to produce the next state.
|
||||||
---
|
---
|
||||||
--- @param prevState The previous state.
|
--- @param currentState The current state.
|
||||||
--- @param nextState The next state.
|
--- @param nextState The next state.
|
||||||
--- @returns A patch that can be applied to the previous state.
|
--- @return A patch that can be applied to the current state.
|
||||||
local function diff(prevState: any, nextState: any): any
|
local function diff(currentState: any, nextState: any): any
|
||||||
if prevState == nextState then
|
if currentState == nextState then
|
||||||
return nil
|
return nil
|
||||||
elseif nextState == nil then
|
elseif nextState == nil then
|
||||||
return None
|
return None
|
||||||
elseif type(prevState) ~= "table" or type(nextState) ~= "table" then
|
elseif type(currentState) ~= "table" or type(nextState) ~= "table" then
|
||||||
return nextState
|
return nextState
|
||||||
end
|
end
|
||||||
|
|
||||||
local patches: { [any]: any } = table.clone(nextState)
|
local patches: { [any]: any } = table.clone(nextState)
|
||||||
|
|
||||||
for key, previous in prevState do
|
for key, previous in currentState do
|
||||||
patches[key] = diff(previous, nextState[key])
|
patches[key] = diff(previous, nextState[key])
|
||||||
end
|
end
|
||||||
|
|
||||||
if config.autoSerialize then
|
if config.autoSerialize then
|
||||||
-- Coerce sparse array patches into dictionaries
|
-- Coerce sparse array patches into dictionaries
|
||||||
if prevState[1] ~= nil or nextState[1] ~= nil then
|
if currentState[1] ~= nil or nextState[1] ~= nil then
|
||||||
patches = stringifySparseArray(patches)
|
patches = stringifySparseArray(patches)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -85,20 +89,20 @@ local function diff(prevState: any, nextState: any): any
|
||||||
return patches
|
return patches
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Applies a patch to a state and returns the next state.
|
--- Applies a patch to a state and returns the resulting value.
|
||||||
---
|
---
|
||||||
--- @param prevState The current state.
|
--- @param currentState The current state.
|
||||||
--- @param statePatch The patches to apply.
|
--- @param statePatch The patches to apply.
|
||||||
--- @returns The next state.
|
--- @return The new state with the patch applied.
|
||||||
local function apply(prevState: any, statePatch: any): any
|
local function apply(currentState: any, statePatch: any): any
|
||||||
if isNone(statePatch) then
|
if isNone(statePatch) then
|
||||||
return nil
|
return nil
|
||||||
elseif type(prevState) ~= "table" or type(statePatch) ~= "table" then
|
elseif type(currentState) ~= "table" or type(statePatch) ~= "table" then
|
||||||
return statePatch
|
return statePatch
|
||||||
end
|
end
|
||||||
|
|
||||||
local nextState = table.clone(prevState)
|
local nextState = table.clone(currentState)
|
||||||
local stateIsArray = prevState[1] ~= nil
|
local stateIsArray = currentState[1] ~= nil
|
||||||
|
|
||||||
for key, value in statePatch do
|
for key, value in statePatch do
|
||||||
-- Diff-checking an array produces a sparse array, which will not be
|
-- Diff-checking an array produces a sparse array, which will not be
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ local LISTENING_FOR_CHANGES: SyncStatus = 1
|
||||||
local atomRegister: { [string]: RegisteredAtom } = {}
|
local atomRegister: { [string]: RegisteredAtom } = {}
|
||||||
local clientRegister: { [any]: { [string]: SyncStatus } } = {}
|
local clientRegister: { [any]: { [string]: SyncStatus } } = {}
|
||||||
local globalUpdates: { { key: string, value: any, previousValue: any } } = {}
|
local globalUpdates: { { key: string, value: any, previousValue: any } } = {}
|
||||||
|
|
||||||
local connection: RBXScriptConnection?
|
local connection: RBXScriptConnection?
|
||||||
|
local onSyncCallback: (client: any, payloads: { SyncPayload }) -> ()?
|
||||||
|
|
||||||
local function registerAtom<T>(key: string, atom: () -> T)
|
local function registerAtom<T>(key: string, atom: () -> T)
|
||||||
if atomRegister[key] then
|
if atomRegister[key] then
|
||||||
|
|
@ -196,6 +198,15 @@ local function createPayloadsForClient(client: any, patchData: any): { SyncPaylo
|
||||||
return payloads
|
return payloads
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Registers atoms to be kept in sync with a specific client. Should be
|
||||||
|
--- called when a player joins the game to start syncing atoms to them. Call
|
||||||
|
--- `unsyncAtomsFromClient` or `unsyncClient` to stop syncing.
|
||||||
|
---
|
||||||
|
--- Each atom should be assigned a unique key, allowing the server to re-use
|
||||||
|
--- patches between multiple clients.
|
||||||
|
---
|
||||||
|
--- @param client The player to sync atoms to.
|
||||||
|
--- @param atoms An object mapping string keys to atoms to be synced.
|
||||||
local function syncAtomsToClient(client: any, atoms: { [string]: () -> any })
|
local function syncAtomsToClient(client: any, atoms: { [string]: () -> any })
|
||||||
if not clientRegister[client] then
|
if not clientRegister[client] then
|
||||||
clientRegister[client] = {}
|
clientRegister[client] = {}
|
||||||
|
|
@ -214,10 +225,19 @@ local function syncAtomsToClient(client: any, atoms: { [string]: () -> any })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Unregisters all atoms from being kept in sync with a specific client.
|
||||||
|
--- Should be called when a player leaves the game.
|
||||||
|
---
|
||||||
|
--- @param client The player to stop syncing atoms to.
|
||||||
local function unsyncClient(client: any)
|
local function unsyncClient(client: any)
|
||||||
clientRegister[client] = nil
|
clientRegister[client] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Unregisters one or more atoms from being kept in sync with a specific
|
||||||
|
--- client.
|
||||||
|
---
|
||||||
|
--- @param client The player to stop syncing atoms to.
|
||||||
|
--- @param keys The keys of the atoms to stop syncing.
|
||||||
local function unsyncAtomsFromClient(client: any, ...: string)
|
local function unsyncAtomsFromClient(client: any, ...: string)
|
||||||
local syncedAtoms = clientRegister[client]
|
local syncedAtoms = clientRegister[client]
|
||||||
|
|
||||||
|
|
@ -235,11 +255,14 @@ local function unsyncAtomsFromClient(client: any, ...: string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onSyncCallback(_client: any, _payloads: { SyncPayload })
|
--- Immediately sends pending updates and initial states to all clients.
|
||||||
warn("[Charm] Override the onSync function by passing a custom function to startSync")
|
--- Normally, updates are sent at the interval specified in `config.interval`.
|
||||||
end
|
--- Calling this function forces all pending updates to be sent immediately.
|
||||||
|
|
||||||
local function flush()
|
local function flush()
|
||||||
|
if not onSyncCallback then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local patches = if config.preserveHistory then createHistoricalPatches() else createPatch()
|
local patches = if config.preserveHistory then createHistoricalPatches() else createPatch()
|
||||||
|
|
||||||
-- While payload objects are technically mutable as they are unique to
|
-- While payload objects are technically mutable as they are unique to
|
||||||
|
|
@ -266,6 +289,8 @@ local function flush()
|
||||||
table.clear(globalUpdates)
|
table.clear(globalUpdates)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Stops automatically syncing registered atoms to clients. Patches will
|
||||||
|
--- no longer be sent to clients until `startSync` is called again.
|
||||||
local function stopSync()
|
local function stopSync()
|
||||||
if connection then
|
if connection then
|
||||||
connection:Disconnect()
|
connection:Disconnect()
|
||||||
|
|
@ -273,6 +298,8 @@ local function stopSync()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Unregisters all atoms from being kept in sync with all clients, and
|
||||||
|
--- stops automatic syncing.
|
||||||
local function reset()
|
local function reset()
|
||||||
table.clear(atomRegister)
|
table.clear(atomRegister)
|
||||||
table.clear(clientRegister)
|
table.clear(clientRegister)
|
||||||
|
|
@ -280,6 +307,11 @@ local function reset()
|
||||||
stopSync()
|
stopSync()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Starts automatically syncing registered atoms to clients. Patches will
|
||||||
|
--- be sent to clients whenever the atoms are updated.
|
||||||
|
---
|
||||||
|
--- @param onSync The function to call when patches can be sent to a client.
|
||||||
|
--- @return `stopSync()`
|
||||||
local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ()): () -> ()
|
local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ()): () -> ()
|
||||||
if connection then
|
if connection then
|
||||||
connection:Disconnect()
|
connection:Disconnect()
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,4 @@ export type SyncPayload =
|
||||||
--- differentiate between a value that was set to `nil` and an unchanged value.
|
--- differentiate between a value that was set to `nil` and an unchanged value.
|
||||||
export type None = { __none: "__none" }
|
export type None = { __none: "__none" }
|
||||||
|
|
||||||
return nil
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,12 @@ test("should not update if the atom value is reverted", function()
|
||||||
assert(times == 1, "computed ran when it should not")
|
assert(times == 1, "computed ran when it should not")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should accept equality function", function()
|
test("should receive previous value", function()
|
||||||
local calls = {}
|
local calls = {}
|
||||||
local src = atom(0)
|
local src = atom(0)
|
||||||
local c1 = computed(function()
|
local c1 = computed(function(current): number
|
||||||
return src()
|
local incoming = src()
|
||||||
end, function(current, incoming)
|
return if current == incoming or incoming % 2 == 1 then current or incoming else incoming
|
||||||
return current == incoming or incoming % 2 == 1
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
local flags = require("./flags")
|
|
||||||
local utils = require("./utils")
|
|
||||||
|
|
||||||
local startBatch = core.startBatch
|
|
||||||
local endBatch = core.endBatch
|
|
||||||
local noYield = utils.noYield
|
|
||||||
|
|
||||||
local function handleBatched(ok: boolean, ...)
|
|
||||||
endBatch()
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
startBatch()
|
|
||||||
return handleBatched((if flags.strict then noYield else pcall)(callback, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return batched
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
-- https://github.com/stackblitz/alien-signals
|
-- https://github.com/stackblitz/alien-signals
|
||||||
-- https://github.com/Nicell/alien-signals-luau
|
-- https://github.com/Nicell/alien-signals-luau
|
||||||
|
|
||||||
|
local alien = require("./alien")
|
||||||
local config = require("./flags")
|
local config = require("./flags")
|
||||||
local system = require("./system")
|
|
||||||
local utils = require("./utils")
|
local utils = require("./utils")
|
||||||
|
|
||||||
export type ReactiveFlags = system.ReactiveFlags
|
export type ReactiveFlags = alien.ReactiveFlags
|
||||||
export type ReactiveNode = system.ReactiveNode
|
export type ReactiveNode = alien.ReactiveNode
|
||||||
export type Link = system.Link
|
export type Link = alien.Link
|
||||||
|
|
||||||
local ReactiveFlags = system.ReactiveFlags
|
local ReactiveFlags = alien.ReactiveFlags
|
||||||
local createReactiveSystem = system.createReactiveSystem
|
local createReactiveSystem = alien.createReactiveSystem
|
||||||
local deepFreeze = utils.deepFreeze
|
local deepFreeze = utils.deepFreeze
|
||||||
local noYield = utils.noYield
|
local noYield = utils.noYield
|
||||||
|
|
||||||
|
|
@ -23,11 +23,11 @@ local PENDING = 0b0100000
|
||||||
local QUEUED = 0b1000000
|
local QUEUED = 0b1000000
|
||||||
local UNTRACKED = 0b10000000
|
local UNTRACKED = 0b10000000
|
||||||
|
|
||||||
export type EffectScope = ReactiveNode & {
|
type EffectScope = ReactiveNode & {
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Effect = ReactiveNode & {
|
type Effect = ReactiveNode & {
|
||||||
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
cleanup: () -> (),
|
cleanup: () -> (),
|
||||||
|
|
@ -36,7 +36,6 @@ export type Effect = ReactiveNode & {
|
||||||
type Computed<T = any> = ReactiveNode & {
|
type Computed<T = any> = ReactiveNode & {
|
||||||
value: T?,
|
value: T?,
|
||||||
getter: (previousValue: T?) -> T,
|
getter: (previousValue: T?) -> T,
|
||||||
equals: ((T, T) -> boolean)?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Atom<T = any> = ReactiveNode & {
|
type Atom<T = any> = ReactiveNode & {
|
||||||
|
|
@ -94,16 +93,6 @@ local function runCleanups(effect: EffectScope)
|
||||||
table.clear(effect.cleanups)
|
table.clear(effect.cleanups)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
|
||||||
if activeSub and (activeSub :: Effect).cleanups then
|
|
||||||
table.insert((activeSub :: Effect).cleanups, cleanup)
|
|
||||||
elseif activeScope then
|
|
||||||
table.insert(activeScope.cleanups, cleanup)
|
|
||||||
elseif not failSilently then
|
|
||||||
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run(effect: Effect, flags: ReactiveFlags): ()
|
local function run(effect: Effect, flags: ReactiveFlags): ()
|
||||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
||||||
runCleanups(effect)
|
runCleanups(effect)
|
||||||
|
|
@ -171,8 +160,7 @@ local function updateComputed(computed: Computed): boolean
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
local equals = computed.equals
|
if oldValue ~= result then
|
||||||
if if equals then not equals(oldValue, result) else oldValue ~= result then
|
|
||||||
computed.value = result
|
computed.value = result
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
|
|
@ -321,26 +309,63 @@ local function unwatched(node: ReactiveNode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function atom<T>(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
--- Creates a reactive atom that acts as both a getter and setter. Calling the
|
||||||
|
--- atom with an argument sets its value, while calling it with no arguments
|
||||||
|
--- returns its current value.
|
||||||
|
---
|
||||||
|
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
||||||
|
--- @param equals A comparator function to determine if the atom's value has changed.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
|
||||||
|
local function atom<T>(value: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||||
if config.frozen then
|
if config.frozen then
|
||||||
deepFreeze(initialValue)
|
deepFreeze(value)
|
||||||
end
|
end
|
||||||
return atomGetterSetter({
|
return atomGetterSetter({
|
||||||
previousValue = initialValue,
|
previousValue = value,
|
||||||
value = initialValue,
|
value = value,
|
||||||
flags = MUTABLE,
|
flags = MUTABLE,
|
||||||
equals = equals,
|
equals = equals,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T, equals: ((T, T) -> boolean)?): () -> T
|
--- Creates a new atom that is computed based on the values of other atoms.
|
||||||
|
--- The returned atom is read-only, and its value is automatically updated when
|
||||||
|
--- any atoms accessed from within the getter function change.
|
||||||
|
---
|
||||||
|
--- If an initial value is provided, the getter will receive this value as its
|
||||||
|
--- first argument on the initial run, instead of `undefined`.
|
||||||
|
---
|
||||||
|
--- @example
|
||||||
|
--- ```ts
|
||||||
|
--- const count = atom(1);
|
||||||
|
--- const plusOne = computed(() => count() + 1);
|
||||||
|
--- print(plusOne()); // 2
|
||||||
|
--- ```
|
||||||
|
---
|
||||||
|
--- @param getter A function that produces the next value.
|
||||||
|
--- @param value An optional initial value for the selector.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
|
||||||
|
local function computed<T>(getter: (previousValue: T?) -> T, value: T?): () -> T
|
||||||
return computedGetter({
|
return computedGetter({
|
||||||
flags = bit32.bor(MUTABLE, DIRTY),
|
flags = bit32.bor(MUTABLE, DIRTY),
|
||||||
getter = getter,
|
getter = getter,
|
||||||
equals = equals,
|
value = value,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates an effect that runs a callback in response to atom state changes.
|
||||||
|
--- An effect tracks which atoms are accessed during its execution, and re-runs
|
||||||
|
--- the callback when those atoms change.
|
||||||
|
---
|
||||||
|
--- The effect callback may return a cleanup function, which gets called once,
|
||||||
|
--- either before the effect re-runs or when it is disposed.
|
||||||
|
---
|
||||||
|
--- If the effect is called within another effect, it will be disposed when the
|
||||||
|
--- outer effect re-runs.
|
||||||
|
---
|
||||||
|
--- @param fn The function to run when dependencies change.
|
||||||
|
--- @return A function for disposing the effect.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||||
local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
||||||
local self: Effect = {
|
local self: Effect = {
|
||||||
fn = fn,
|
fn = fn,
|
||||||
|
|
@ -365,6 +390,14 @@ local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
||||||
return self.cleanup
|
return self.cleanup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates an effect scope that can capture multiple reactive effects (i.e.
|
||||||
|
--- computed, effect, subscribe) created within it so that they can be disposed
|
||||||
|
--- together.
|
||||||
|
---
|
||||||
|
--- @param fn A function that may create reactive effects.
|
||||||
|
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
||||||
|
--- @return A function for disposing all effects created within the scope.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||||
local self: EffectScope = {
|
local self: EffectScope = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
|
|
@ -386,6 +419,81 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () ->
|
||||||
return effectStop(self)
|
return effectStop(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Registers a cleanup function to the current active effect or effect scope.
|
||||||
|
--- If there is no active effect or scope, a warning is logged and the callback
|
||||||
|
--- is not registered, unless `failSilently` is set to true.
|
||||||
|
---
|
||||||
|
--- @param callback The cleanup function to register.
|
||||||
|
--- @param failSilently If true, suppresses the warning when there is no active effect or scope. Defaults to false.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||||
|
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
||||||
|
if activeSub and (activeSub :: Effect).cleanups then
|
||||||
|
table.insert((activeSub :: Effect).cleanups, cleanup)
|
||||||
|
elseif activeScope then
|
||||||
|
table.insert(activeScope.cleanups, cleanup)
|
||||||
|
elseif not failSilently then
|
||||||
|
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Runs the callback without subscribing to atom updates or capturing inner
|
||||||
|
--- effects in the parent effect or scope.
|
||||||
|
---
|
||||||
|
--- For an alternative that still cleans up inner effects, use `peek` instead.
|
||||||
|
---
|
||||||
|
--- @param callback The callback function.
|
||||||
|
--- @param args Arguments to pass to the callback.
|
||||||
|
--- @return The return value of the callback.
|
||||||
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
local prevSub = setCurrentSub(nil)
|
||||||
|
local prevScope = setCurrentScope(nil)
|
||||||
|
local results: { any } = { pcall(callback, ...) }
|
||||||
|
setCurrentSub(prevSub)
|
||||||
|
setCurrentScope(prevScope)
|
||||||
|
if not results[1] then
|
||||||
|
error(results[2])
|
||||||
|
end
|
||||||
|
return unpack(results, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Runs the callback without subscribing to atom updates, but still captures
|
||||||
|
--- inner effects so that they dispose with the parent effect.
|
||||||
|
---
|
||||||
|
--- To avoid capturing inner effects, use `untracked` instead.
|
||||||
|
---
|
||||||
|
--- @param callback The callback function.
|
||||||
|
--- @param args Arguments to pass to the callback.
|
||||||
|
--- @return The return value of the callback.
|
||||||
|
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
if not activeSub or bit32.btest(activeSub.flags, UNTRACKED) then
|
||||||
|
return callback(...)
|
||||||
|
else
|
||||||
|
activeSub.flags = bit32.bor(activeSub.flags, UNTRACKED)
|
||||||
|
local results: { any } = { pcall(callback, ...) }
|
||||||
|
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(UNTRACKED))
|
||||||
|
if not results[1] then
|
||||||
|
error(results[2])
|
||||||
|
end
|
||||||
|
return unpack(results, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Combines multiple atom updates into a single "commit" that runs effects at
|
||||||
|
--- the end of the provided callback.
|
||||||
|
---
|
||||||
|
--- @param callback A function that performs multiple atom updates.
|
||||||
|
--- @param args Arguments to pass to the callback.
|
||||||
|
--- @return The return value of the callback.
|
||||||
|
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
startBatch()
|
||||||
|
local results: { any } = { (if config.strict then noYield else pcall)(callback, ...) }
|
||||||
|
endBatch()
|
||||||
|
if not results[1] then
|
||||||
|
error(results[2])
|
||||||
|
end
|
||||||
|
return unpack(results, 2)
|
||||||
|
end
|
||||||
|
|
||||||
local reactiveSystem = createReactiveSystem({
|
local reactiveSystem = createReactiveSystem({
|
||||||
update = update,
|
update = update,
|
||||||
notify = notify,
|
notify = notify,
|
||||||
|
|
@ -415,4 +523,7 @@ return {
|
||||||
computed = computed,
|
computed = computed,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
effectScope = effectScope,
|
effectScope = effectScope,
|
||||||
|
untracked = untracked,
|
||||||
|
peek = peek,
|
||||||
|
batched = batched,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
172
packages/charm/src/index.d.ts
vendored
172
packages/charm/src/index.d.ts
vendored
|
|
@ -1,6 +1,11 @@
|
||||||
type Key = string | number | symbol;
|
type Key = string | number | symbol;
|
||||||
type Cleanup = () => void;
|
type Cleanup = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reactive atom that acts as both a getter and setter. Calling the atom with
|
||||||
|
* an argument sets its value, while calling it with no arguments returns its
|
||||||
|
* current value.
|
||||||
|
*/
|
||||||
export interface Atom<T> {
|
export interface Atom<T> {
|
||||||
(value: T | ((value: T) => T)): T;
|
(value: T | ((value: T) => T)): T;
|
||||||
(): T;
|
(): T;
|
||||||
|
|
@ -10,29 +15,130 @@ export type Selector<T> = () => T;
|
||||||
|
|
||||||
export type Equals<T> = (current: T, incoming: T) => boolean;
|
export type Equals<T> = (current: T, incoming: T) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global flags that modify the behavior of Charm's reactive system.
|
||||||
|
*/
|
||||||
export const flags: {
|
export const flags: {
|
||||||
|
/**
|
||||||
|
* Enforces synchronous, non-yielding behavior in atoms and effects.
|
||||||
|
* Also enables state validation in Charm Sync to catch sync errors early.
|
||||||
|
* Enabled if the optimization level is below 2, which is true in studio.
|
||||||
|
*/
|
||||||
strict: boolean;
|
strict: boolean;
|
||||||
|
/**
|
||||||
|
* Enforces immutability of atoms by deep-freezing table values. Enabled
|
||||||
|
* if the optimization level is below 2, which is true in studio.
|
||||||
|
*/
|
||||||
frozen: boolean;
|
frozen: boolean;
|
||||||
|
/**
|
||||||
|
* Whether inner effects should clean up after themselves when their
|
||||||
|
* parent effect re-runs. This is usually desirable, but can be disabled
|
||||||
|
* to revert to the old behavior. Defaults to `true`.
|
||||||
|
*/
|
||||||
trackInnerEffects: boolean;
|
trackInnerEffects: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive atom that acts as both a getter and setter. Calling the
|
||||||
|
* atom with an argument sets its value, while calling it with no arguments
|
||||||
|
* returns its current value.
|
||||||
|
*
|
||||||
|
* @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
||||||
|
* @param equals A comparator function to determine if the atom's value has changed.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
|
||||||
|
*/
|
||||||
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
||||||
// Overload for undefined initial value
|
|
||||||
export function atom<T>(): Atom<T | undefined>;
|
export function atom<T>(): Atom<T | undefined>;
|
||||||
|
|
||||||
export function computed<T>(getter: (previousValue?: T) => T, equals?: Equals<T>): () => T;
|
/**
|
||||||
|
* Creates a new atom that is computed based on the values of other atoms.
|
||||||
|
* The returned atom is read-only, and its value is automatically updated when
|
||||||
|
* any atoms accessed from within the getter function change.
|
||||||
|
*
|
||||||
|
* If an initial value is provided, the getter will receive this value as its
|
||||||
|
* first argument on the initial run, instead of `undefined`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const count = atom(1);
|
||||||
|
* const plusOne = computed(() => count() + 1);
|
||||||
|
* print(plusOne()); // 2
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param getter A function that produces the next value.
|
||||||
|
* @param value An optional initial value for the selector.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
|
||||||
|
*/
|
||||||
|
export function computed<T>(getter: (previousValue?: T) => T): () => T;
|
||||||
|
export function computed<T>(getter: (previousValue: T) => T, value: T): () => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an effect that runs a callback in response to atom state changes.
|
||||||
|
* An effect tracks which atoms are accessed during its execution, and re-runs
|
||||||
|
* the callback when those atoms change.
|
||||||
|
*
|
||||||
|
* The effect callback may return a cleanup function, which gets called once,
|
||||||
|
* either before the effect re-runs or when it is disposed.
|
||||||
|
*
|
||||||
|
* If the effect is called within another effect, it will be disposed when the
|
||||||
|
* outer effect re-runs.
|
||||||
|
*
|
||||||
|
* @param callback The function to run when dependencies change.
|
||||||
|
* @returns A function for disposing the effect.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||||
|
*/
|
||||||
export function effect(callback: (dispose: Cleanup) => Cleanup | void): Cleanup;
|
export function effect(callback: (dispose: Cleanup) => Cleanup | void): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an effect scope that can capture multiple reactive effects (i.e.
|
||||||
|
* computed, effect, subscribe) created within it so that they can be disposed
|
||||||
|
* together.
|
||||||
|
*
|
||||||
|
* @param callback A function that may create reactive effects.
|
||||||
|
* @param detached If true, the scope will not be automatically disposed of
|
||||||
|
* when the parent scope is disposed. Defaults to false.
|
||||||
|
* @returns A function for disposing all effects created within the scope.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||||
|
*/
|
||||||
export function effectScope(callback: () => Cleanup | void, detached?: boolean): Cleanup;
|
export function effectScope(callback: () => Cleanup | void, detached?: boolean): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an effect that only runs the callback when the value returned by the
|
||||||
|
* getter changes. The callback receives the new value and the previous value
|
||||||
|
* as arguments.
|
||||||
|
*
|
||||||
|
* @param getter A function that returns the value to watch.
|
||||||
|
* @param callback The function to run when the value changes.
|
||||||
|
* @returns A function for disposing the effect.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||||
|
*/
|
||||||
|
export function subscribe<T>(getter: () => T, callback: (value: T, previousValue: T) => void): Cleanup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an effect that runs the callback one immediately, and then again
|
||||||
|
* when the value returned by the getter changes. The callback receives the new
|
||||||
|
* value, the previous value, and a cleanup function as arguments.
|
||||||
|
*
|
||||||
|
* @param getter A function that returns the value to watch.
|
||||||
|
* @param callback The function to run when the value changes.
|
||||||
|
* @returns A function for disposing the effect.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||||
|
*/
|
||||||
export function listen<T>(
|
export function listen<T>(
|
||||||
getter: () => T,
|
getter: () => T,
|
||||||
listener: (value: T, previousValue: T | undefined, dispose: Cleanup) => void,
|
callback: (value: T, previousValue: T | undefined, dispose: Cleanup) => void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
export function subscribe<T>(getter: () => T, listener: (value: T, previousValue: T) => void): Cleanup;
|
/**
|
||||||
|
* Creates a new atom that is computed by mapping over the entries of an
|
||||||
|
* existing atom. The returned atom is read-only, and its value is
|
||||||
|
* automatically updated when the source atom changes.
|
||||||
|
*
|
||||||
|
* @param getter A function that returns the table to map over.
|
||||||
|
* @param mapper A function that transforms each value-key pair from the source
|
||||||
|
* into a new value or value-key pair for the resulting table.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
||||||
|
*/
|
||||||
export function mapped<VI, KI, VO, KO = KI>(
|
export function mapped<VI, KI, VO, KO = KI>(
|
||||||
getter: () => Map<KI, VI> | ReadonlyMap<KI, VI>,
|
getter: () => Map<KI, VI> | ReadonlyMap<KI, VI>,
|
||||||
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
||||||
|
|
@ -48,6 +154,19 @@ export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
|
||||||
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
||||||
): () => { readonly [K in KO]: VO };
|
): () => { readonly [K in KO]: VO };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the observer function for each unique key in the table returned by the
|
||||||
|
* getter. The observer is called once for each key, and again when a new key
|
||||||
|
* is added.
|
||||||
|
*
|
||||||
|
* The observer may return a cleanup function, which will be called when the
|
||||||
|
* key is removed or when the entire observer is disposed.
|
||||||
|
*
|
||||||
|
* @param getter A function that returns the table to observe.
|
||||||
|
* @param observer A function that is called for each key in the table.
|
||||||
|
* @returns A function for disposing the observer.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
|
||||||
|
*/
|
||||||
export function observe<V, K = number>(
|
export function observe<V, K = number>(
|
||||||
getter: () => Map<K, V> | ReadonlyMap<K, V> | readonly V[],
|
getter: () => Map<K, V> | ReadonlyMap<K, V> | readonly V[],
|
||||||
observer: (value: V, key: K) => Cleanup | void,
|
observer: (value: V, key: K) => Cleanup | void,
|
||||||
|
|
@ -58,13 +177,48 @@ export function observe<V, K extends Key>(
|
||||||
observer: (value: V, key: K) => Cleanup | void,
|
observer: (value: V, key: K) => Cleanup | void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
export function batched<Args extends any[], Result>(
|
/**
|
||||||
callback: (...args: Args) => Result,
|
* Combines multiple atom updates into a single "commit" that runs effects at
|
||||||
...args: Args
|
* the end of the provided callback.
|
||||||
): (...args: Args) => Result;
|
*
|
||||||
|
* @param callback A function that performs multiple atom updates.
|
||||||
|
* @param args Arguments to pass to the callback.
|
||||||
|
* @returns The return value of the callback.
|
||||||
|
*/
|
||||||
|
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the callback without subscribing to atom updates or capturing inner
|
||||||
|
* effects in the parent effect or scope.
|
||||||
|
*
|
||||||
|
* For an alternative that still cleans up inner effects, use `peek` instead.
|
||||||
|
*
|
||||||
|
* @param callback The callback function.
|
||||||
|
* @param args Arguments to pass to the callback.
|
||||||
|
* @returns The return value of the callback.
|
||||||
|
*/
|
||||||
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the callback without subscribing to atom updates, but still captures
|
||||||
|
* inner effects so that they dispose with the parent effect.
|
||||||
|
*
|
||||||
|
* To avoid capturing inner effects, use `untracked` instead.
|
||||||
|
*
|
||||||
|
* @param callback The callback function.
|
||||||
|
* @param args Arguments to pass to the callback.
|
||||||
|
* @returns The return value of the callback.
|
||||||
|
*/
|
||||||
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a cleanup function to the current active effect or effect scope.
|
||||||
|
* If there is no active effect or scope, a warning is logged and the callback
|
||||||
|
* is not registered, unless `failSilently` is set to true.
|
||||||
|
*
|
||||||
|
* @param callback The cleanup function to register.
|
||||||
|
* @param failSilently If true, suppresses the warning when there is no active
|
||||||
|
* effect or scope. Defaults to false.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||||
|
*/
|
||||||
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,33 @@
|
||||||
local batched = require("@self/batched")
|
|
||||||
local core = require("@self/core")
|
local core = require("@self/core")
|
||||||
local flags = require("@self/flags")
|
local flags = require("@self/flags")
|
||||||
local listen = require("@self/listen")
|
local listen = require("@self/listen")
|
||||||
local mapped = require("@self/mapped")
|
local mapped = require("@self/mapped")
|
||||||
local observe = require("@self/observe")
|
local observe = require("@self/observe")
|
||||||
local peek = require("@self/peek")
|
|
||||||
local subscribe = require("@self/subscribe")
|
local subscribe = require("@self/subscribe")
|
||||||
local types = require("@self/types")
|
local types = require("@self/types")
|
||||||
local untracked = require("@self/untracked")
|
|
||||||
local utils = require("@self/utils")
|
local utils = require("@self/utils")
|
||||||
|
|
||||||
|
--- A reactive atom that acts as both a getter and setter. Calling the atom with
|
||||||
|
--- an argument sets its value, while calling it with no arguments returns its
|
||||||
|
--- current value.
|
||||||
export type Atom<T = any> = types.Atom<T>
|
export type Atom<T = any> = types.Atom<T>
|
||||||
|
|
||||||
export type Selector<T = any> = types.Selector<T>
|
export type Selector<T = any> = types.Selector<T>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
core = core,
|
core = core,
|
||||||
flags = flags,
|
flags = flags,
|
||||||
atom = core.atom,
|
atom = core.atom,
|
||||||
|
batched = core.batched,
|
||||||
computed = core.computed,
|
computed = core.computed,
|
||||||
effect = core.effect,
|
effect = core.effect,
|
||||||
effectScope = core.effectScope,
|
effectScope = core.effectScope,
|
||||||
onCleanup = core.onCleanup,
|
onCleanup = core.onCleanup,
|
||||||
batched = batched,
|
peek = core.peek,
|
||||||
|
untracked = core.untracked,
|
||||||
listen = listen,
|
listen = listen,
|
||||||
mapped = mapped,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
peek = peek,
|
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
untracked = untracked,
|
|
||||||
deepFreeze = utils.deepFreeze,
|
deepFreeze = utils.deepFreeze,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local peek = require("./peek")
|
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
local peek = core.peek
|
||||||
|
|
||||||
local function listen<T>(getter: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
--- Creates an effect that runs the callback one immediately, and then again
|
||||||
|
--- when the value returned by the getter changes. The callback receives the new
|
||||||
|
--- value, the previous value, and a cleanup function as arguments.
|
||||||
|
---
|
||||||
|
--- @param getter A function that returns the value to watch.
|
||||||
|
--- @param callback The function to run when the value changes.
|
||||||
|
--- @return A function for disposing the effect.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||||
|
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||||
local tracked = computed(function()
|
local tracked = computed(function()
|
||||||
return getter()
|
return getter()
|
||||||
end)
|
end)
|
||||||
|
|
@ -19,7 +27,7 @@ local function listen<T>(getter: () -> T, listener: (state: T, previous: T?, dis
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
peek(listener, current, previous, dispose)
|
peek(callback, current, previous, dispose)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposed then
|
if disposed then
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
||||||
local cachedKey = keyCache[key]
|
local cachedKey = keyCache[key]
|
||||||
|
|
||||||
if cachedKey then
|
if cachedKey then
|
||||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
if currentOutput == previousOutput then
|
||||||
|
currentOutput = table.clone(currentOutput)
|
||||||
|
end
|
||||||
currentOutput[cachedKey] = nil
|
currentOutput[cachedKey] = nil
|
||||||
keyCache[key] = nil
|
keyCache[key] = nil
|
||||||
end
|
end
|
||||||
|
|
@ -50,7 +52,9 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
||||||
local valueOut, keyOut = map(value, key)
|
local valueOut, keyOut = map(value, key)
|
||||||
|
|
||||||
if currentOutput[keyOut] ~= valueOut then
|
if currentOutput[keyOut] ~= valueOut then
|
||||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
if currentOutput == previousOutput then
|
||||||
|
currentOutput = table.clone(currentOutput)
|
||||||
|
end
|
||||||
currentOutput[keyOut] = valueOut
|
currentOutput[keyOut] = valueOut
|
||||||
keyCache[key] = keyOut
|
keyCache[key] = keyOut
|
||||||
end
|
end
|
||||||
|
|
@ -60,4 +64,13 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return mapped :: Mapped
|
--- Creates a new atom that is computed by mapping over the entries of an
|
||||||
|
--- existing atom. The returned atom is read-only, and its value is
|
||||||
|
--- automatically updated when the source atom changes.
|
||||||
|
---
|
||||||
|
--- @param getter A function that returns the table to map over.
|
||||||
|
--- @param mapper A function that transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
||||||
|
local mapped = mapped :: Mapped
|
||||||
|
|
||||||
|
return mapped
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
|
local flags = require("./flags")
|
||||||
|
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
local effectScope = core.effectScope
|
local effectScope = core.effectScope
|
||||||
|
|
@ -6,6 +7,17 @@ local onCleanup = core.onCleanup
|
||||||
local startBatch = core.startBatch
|
local startBatch = core.startBatch
|
||||||
local endBatch = core.endBatch
|
local endBatch = core.endBatch
|
||||||
|
|
||||||
|
--- Calls the observer function for each unique key in the table returned by the
|
||||||
|
--- getter. The observer is called once for each key, and again when a new key
|
||||||
|
--- is added.
|
||||||
|
---
|
||||||
|
--- The observer may return a cleanup function, which will be called when the
|
||||||
|
--- key is removed or when the entire observer is disposed.
|
||||||
|
---
|
||||||
|
--- @param getter A function that returns the table to observe.
|
||||||
|
--- @param observer A function that is called for each key in the table.
|
||||||
|
--- @return A function for disposing the observer.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
|
||||||
local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||||
local scopes: { [K]: () -> () } = {}
|
local scopes: { [K]: () -> () } = {}
|
||||||
local currentItems: { [K]: V } = {}
|
local currentItems: { [K]: V } = {}
|
||||||
|
|
@ -45,7 +57,9 @@ local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key:
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
onCleanup(disposeScopes, true)
|
if flags.trackInnerEffects then
|
||||||
|
onCleanup(disposeScopes, true)
|
||||||
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
disposeEffect()
|
disposeEffect()
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local UNTRACKED = core.ReactiveFlags.Untracked
|
|
||||||
local getCurrentSub = core.getCurrentSub
|
|
||||||
|
|
||||||
local function handlePeek(sub: core.ReactiveNode?, ok: boolean, ...)
|
|
||||||
if sub then
|
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED))
|
|
||||||
end
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
local sub = getCurrentSub()
|
|
||||||
if not sub or bit32.btest(sub.flags, UNTRACKED) then
|
|
||||||
return callback(...)
|
|
||||||
else
|
|
||||||
sub.flags = bit32.bor(sub.flags, UNTRACKED)
|
|
||||||
end
|
|
||||||
return handlePeek(sub, pcall(callback, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return peek
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local peek = require("./peek")
|
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
local peek = core.peek
|
||||||
|
|
||||||
local function subscribe<T>(getter: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
--- Creates an effect that only runs the callback when the value returned by the
|
||||||
|
--- getter changes. The callback receives the new value and the previous value
|
||||||
|
--- as arguments.
|
||||||
|
---
|
||||||
|
--- @param getter A function that returns the value to watch.
|
||||||
|
--- @param callback The function to run when the value changes.
|
||||||
|
--- @return A function for disposing the effect.
|
||||||
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||||
|
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
|
||||||
local tracked = computed(function()
|
local tracked = computed(function()
|
||||||
return getter()
|
return getter()
|
||||||
end)
|
end)
|
||||||
|
|
@ -18,7 +26,7 @@ local function subscribe<T>(getter: () -> T, listener: (state: T, lastState: T)
|
||||||
if mounted then
|
if mounted then
|
||||||
mounted = false
|
mounted = false
|
||||||
else
|
else
|
||||||
peek(listener, current, previous)
|
peek(callback, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
type Update<T> = ((previous: T) -> T) | T
|
type Update<T> = ((previous: T) -> T) | T
|
||||||
|
|
||||||
export type Atom<T> = (...Update<T>) -> T
|
export type Atom<T> = (...Update<T>) -> T
|
||||||
|
|
||||||
export type Selector<T> = () -> T
|
export type Selector<T> = () -> T
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local setCurrentSub = core.setCurrentSub
|
|
||||||
local setCurrentScope = core.setCurrentScope
|
|
||||||
local getCurrentSub = core.getCurrentSub
|
|
||||||
local getCurrentScope = core.getCurrentScope
|
|
||||||
|
|
||||||
local function handleUntracked(
|
|
||||||
prevSub: core.ReactiveNode?,
|
|
||||||
prevScope: core.Effect | core.EffectScope?,
|
|
||||||
ok: boolean,
|
|
||||||
...
|
|
||||||
)
|
|
||||||
if prevSub then
|
|
||||||
setCurrentSub(prevSub)
|
|
||||||
elseif prevScope then
|
|
||||||
setCurrentScope(prevScope)
|
|
||||||
end
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
if getCurrentSub() then
|
|
||||||
return handleUntracked(setCurrentSub(nil), nil, pcall(callback, ...))
|
|
||||||
elseif getCurrentScope() then
|
|
||||||
return handleUntracked(nil, setCurrentScope(nil), pcall(callback, ...))
|
|
||||||
else
|
|
||||||
return callback(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return untracked
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue