mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
chore: prepare release candidate
This commit is contained in:
parent
29d8add265
commit
ed9566bfff
14 changed files with 67 additions and 59 deletions
64
README.md
64
README.md
|
|
@ -52,8 +52,9 @@
|
|||
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
||||
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
||||
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
||||
- [`trigger(callback)`](#triggercallback)
|
||||
- [`flags`](#flags)
|
||||
- [`recursive()`](#recursive)
|
||||
- [`trigger(callback)`](#triggercallback)
|
||||
- [`flags`](#flags)
|
||||
- [Client-Server Sync](#client-server-sync)
|
||||
- [Installation](#installation-1)
|
||||
- [Quick Start](#quick-start)
|
||||
|
|
@ -229,9 +230,10 @@ Effects are fundamental to reactivity, allowing you to react to signal updates.
|
|||
```luau
|
||||
local getCounter, setCounter = signal(0)
|
||||
|
||||
-- Count is 0
|
||||
effect(function()
|
||||
print(`Count is {getCounter()}`)
|
||||
end) -- Count is 0
|
||||
end)
|
||||
|
||||
setCounter(1) -- Count is 1
|
||||
```
|
||||
|
|
@ -269,7 +271,7 @@ setCounter(2) -- Outer: 2, Inner: 2
|
|||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To run code that is "detached" from the parent effect or scope, use `untracked()` or a detached effect scope. If you suspect that the new nested effect behavior is causing issues with migration, try disabling the `globals.trackInnerEffects` flag to assist with debugging.
|
||||
> To run code that is "detached" from the parent effect or scope, use `untracked()` or a detached effect scope. If you suspect that the new nested effect behavior is causing issues with migration, try disabling the `flags.trackInnerEffects` flag to assist with debugging.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -278,18 +280,17 @@ setCounter(2) -- Outer: 2, Inner: 2
|
|||
In case you want to read signals but don't want to subscribe to them, you can use `untracked()` to essentially run code _outside_ the current effect, preventing signals and effects in the callback from being tracked.
|
||||
|
||||
```luau
|
||||
local getCounter, setCounter = signal(0)
|
||||
local getRuns, setRuns = signal(0)
|
||||
local getTracked, setTracked = signal(0)
|
||||
local getUntracked, setUntracked = signal(0)
|
||||
|
||||
-- Tracked: 0, Untracked: 0
|
||||
effect(function()
|
||||
local runs = untracked(getRuns) + 1
|
||||
setRuns(runs)
|
||||
print(`Count is {getCounter()}, ran {runs} times`)
|
||||
end) -- Count is 0, ran 1 time
|
||||
print(`Tracked: {getTracked()}, Untracked: {untracked(getUntracked)}`)
|
||||
end)
|
||||
|
||||
setCounter(10) -- Count is 10, ran 2 times
|
||||
setRuns(10) -- No output
|
||||
setCounter(20) -- Count is 20, ran 11 times
|
||||
setTracked(1) -- Tracked: 1, Untracked: 0
|
||||
setUntracked(1) -- No output
|
||||
setTracked(2) -- Tracked: 2, Untracked: 1
|
||||
```
|
||||
|
||||
Because `untracked()` executes the callback outside the current effect, nested effects created in the callback do not get tracked by the parent effect:
|
||||
|
|
@ -319,9 +320,10 @@ Similar to `untracked()`, but does not prevent the parent effect from tracking n
|
|||
```luau
|
||||
local getCounter, setCounter = signal(0)
|
||||
|
||||
-- Count is 0
|
||||
local disposeOuter = effect(function()
|
||||
print(`Count is {peek(getCounter)}`)
|
||||
end) -- Count is 0
|
||||
end)
|
||||
|
||||
setCounter(1) -- No output; count was accessed in peek()
|
||||
```
|
||||
|
|
@ -384,9 +386,10 @@ The callback also receives the previous value, or `nil` when running for the fir
|
|||
```luau
|
||||
local getCounter, setCounter = signal(0)
|
||||
|
||||
-- Count is 0 (was nil)
|
||||
listen(getCounter, function(count, prevCount)
|
||||
print(`Count is {count} (was {prevCount})`)
|
||||
end) -- Count is 0 (was nil)
|
||||
end)
|
||||
|
||||
setCounter(1) -- Count is 1 (was 0)
|
||||
```
|
||||
|
|
@ -421,12 +424,13 @@ Note that nested effects can be created inside `subscribe`, and they will clean
|
|||
```luau
|
||||
local getItems, setItems = signal({ a = 0, b = 0 })
|
||||
|
||||
-- Added a, Added b
|
||||
observe(getItems, function(value, key)
|
||||
print(`Added {key}`)
|
||||
return function()
|
||||
print(`Removed {key}`)
|
||||
end
|
||||
end) -- Added a, Added b
|
||||
end)
|
||||
|
||||
setItems({ a = 0, c = 0 }) -- Removed b, Added c
|
||||
```
|
||||
|
|
@ -547,7 +551,9 @@ effect(function()
|
|||
setCounter(function(count)
|
||||
return math.min(count + 1, 3)
|
||||
end)
|
||||
end) -- 0, 1, 2, 3
|
||||
end)
|
||||
|
||||
-- Output: 0, 1, 2, 3
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -675,7 +681,7 @@ end)
|
|||
|
||||
To sync the client with the server's state, call `client.addSignals` with a table of writable signals (setter functions or atoms) whose keys match their server counterparts.
|
||||
|
||||
After the client receives updates from the server, call `client.hydrate` to patch the client's signals with the incoming state updates.
|
||||
After the client receives updates from the server, call `client.patch` to patch the client's signals with the incoming state updates.
|
||||
|
||||
```luau
|
||||
-- Add signal setters or atoms
|
||||
|
|
@ -686,7 +692,7 @@ client.addSignals({
|
|||
|
||||
-- Update client signals when receiving updates from the server
|
||||
syncEvent.OnClientEvent:Connect(function(updates)
|
||||
client.hydrate(updates)
|
||||
client.patch(updates)
|
||||
end)
|
||||
```
|
||||
|
||||
|
|
@ -798,7 +804,7 @@ server.flush()
|
|||
|
||||
Subscribes the given writable signals to the states with the corresponding keys on the server. When the server sends updates, the functions associated with each key in the state will be called with the patched values.
|
||||
|
||||
You should pass either pass signal setter functions or atoms to this function:
|
||||
You can pass either writable signals or atoms to this function:
|
||||
|
||||
```luau
|
||||
client.addSignals({
|
||||
|
|
@ -823,11 +829,11 @@ client.removeSignals("name")
|
|||
|
||||
---
|
||||
|
||||
### `client.hydrate(updates)`
|
||||
### `client.patch(updates)`
|
||||
|
||||
The `hydrate` function patches the client's state with the updates sent from the server. The initial update sent by the server will be the full state, and later updates will only include the values that changed.
|
||||
The `patch` function patches the client's state with the updates sent from the server. The initial update sent by the server will be the full state, and later updates will only include the values that changed.
|
||||
|
||||
You should call `hydrate` when receiving updates from the server from a remote event:
|
||||
You should call `patch` when receiving updates from the server from a remote event:
|
||||
|
||||
```luau
|
||||
client.addSignals({
|
||||
|
|
@ -836,7 +842,7 @@ client.addSignals({
|
|||
})
|
||||
|
||||
syncEvent.OnClientEvent:Connect(function(updates)
|
||||
client.hydrate(updates)
|
||||
client.patch(updates)
|
||||
end)
|
||||
```
|
||||
|
||||
|
|
@ -885,22 +891,22 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
|||
4. Nested effects automatically clean up when the parent effect re-runs or gets disposed. In other words, all effects created during the execution of another effect will be added as a "child" and clean up with the parent effect. This might cause issues in code that relied on the old behavior, where effects were detached from the parent.
|
||||
- This feature applies to all reaction APIs, including the listener function in `subscribe()` and the observer function in `observe()`.
|
||||
- Effects that should not be tracked by a parent effect/scope should be wrapped in [`untracked()`](#untrackedcallback).
|
||||
- This feature can introduce runtime bugs in migrated code. If you suspect this to be the cause, to help identify the issue, you can disable this feature by setting [`globals.trackInnerEffects`](#flags) to `false`.
|
||||
- This feature can introduce runtime bugs in migrated code. If you suspect this to be the cause, to help identify the issue, you can disable this feature by setting [`flags.trackInnerEffects`](#flags) to `false`.
|
||||
|
||||
> [!NOTE]
|
||||
> An example of nested effects causing a bug is an old implementation of [`VideCharm.useAtom`](./packages/vide-charm/src/init.luau) that did not wrap the source update in `untracked()`. Because Vide effects run immediately after a source updates, Vide will notify components during the execution of the Charm effect in `useAtom`.
|
||||
>
|
||||
> This meant effects created as a side effect of a source update would implicitly get added as a child of the `useAtom` effect, and they could get disposed at the wrong time and desync UI.
|
||||
|
||||
5. Recursion is disallowed in effects and computed signals now that Charm uses the [alien-signals algorithm](https://github.com/stackblitz/alien-signals). This change may introduce bugs in code relying on the old behavior.
|
||||
5. Recursion is now disallowed in effects and computed signals by default. This change may introduce bugs in code relying on the old behavior.
|
||||
- Recursive checks are opt-out. You can allow recursion for a specific effect by calling [`recursive()`](#recursive) at the top of the effect callback.
|
||||
|
||||
6. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
|
||||
- Added [`signal()`](#signalinitialvalue-equals) to make reads and writes more explicit
|
||||
- Added [`listen()`](#listengetter-callback) for running a subscription once immediately
|
||||
- Added [`listen()`](#listengetter-callback) to create a subscription that runs once immediately
|
||||
- Added [`effectScope()`](#effectscopecallback) for collecting and cleaning up multiple effects at once
|
||||
- Added [`onCleanup()`](#oncleanupcallback-failsilently) for running code when the currently-running effect, subscription, or observer cleans up
|
||||
- Added [`trigger()`](#triggercallback) for triggering updates for table mutations
|
||||
- Added [`onCleanup()`](#oncleanupcallback-failsilently) to bind a cleanup function to the active effect
|
||||
- Added [`trigger()`](#triggercallback) to trigger updates for table mutations
|
||||
- The [`computed()`](#computedgetter) callback now gets called with the previous computed value
|
||||
- Optimized `computed()` to use lazy evaluation instead of eager updates
|
||||
- Optimized `mapped()` to only map values that changed in the original table
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/charm-sync",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0-rc.1",
|
||||
"description": "Simple server-to-client sync for Charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ test("should hydrate atom", function()
|
|||
local a = Charm.atom(1)
|
||||
|
||||
client.addSignals({ a = a })
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "init", data = { a = 2 } },
|
||||
})
|
||||
|
||||
|
|
@ -25,13 +25,13 @@ test("should merge patch", function()
|
|||
local previous = a()
|
||||
|
||||
client.addSignals({ a = a })
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "init", data = { a = a() } },
|
||||
})
|
||||
|
||||
assert(a() == previous, "should set a to the initial value")
|
||||
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "patch", data = { a = { b = 3 } } },
|
||||
})
|
||||
|
||||
|
|
@ -44,14 +44,14 @@ test("should fix sparse array patches", function()
|
|||
local a = Charm.atom({ 1 })
|
||||
|
||||
client.addSignals({ a = a })
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "init", data = { a = a() } },
|
||||
})
|
||||
|
||||
-- This patch would be the result of a diff between { 1 } and { 1, 2 } that
|
||||
-- was JSON-serialized and deserialized. The client should be able to catch
|
||||
-- this and convert the string key back to a number.
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "patch", data = { a = { ["2"] = 2 } } },
|
||||
})
|
||||
|
||||
|
|
@ -69,11 +69,11 @@ test("should merge multiple patches", function()
|
|||
table.insert(calls, value)
|
||||
end)
|
||||
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "init", data = { a = a() } },
|
||||
})
|
||||
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "patch", data = { a = 2 } },
|
||||
{ type = "patch", data = { a = 3 } },
|
||||
})
|
||||
|
|
@ -97,11 +97,11 @@ test("should batch patches", function()
|
|||
states.a = a()
|
||||
end)
|
||||
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "init", data = { a = a(), b = b() } },
|
||||
})
|
||||
|
||||
client.hydrate({
|
||||
client.patch({
|
||||
{ type = "patch", data = { a = 1, b = 2 } },
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test("should replicate server state to client", function()
|
|||
server.addSignalsToClient(target, { a = serverA, b = serverB })
|
||||
server.connect(function(syncTarget, payloads)
|
||||
if syncTarget == target then
|
||||
client.hydrate(payloads)
|
||||
client.patch(payloads)
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ test("should replicate historical server state to client", function()
|
|||
server.addSignalsToClient(target, { a = serverA, b = serverB })
|
||||
server.connect(function(syncTarget, payloads)
|
||||
if syncTarget == target then
|
||||
client.hydrate(payloads)
|
||||
client.patch(payloads)
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ end
|
|||
--- initializing it with the full state or merging a partial patch.
|
||||
---
|
||||
--- @param payloads The state updates received from the server.
|
||||
local function hydrate(payloads: { SyncPayload })
|
||||
local function patch(payloads: { SyncPayload })
|
||||
debug.profilebegin("CharmSync")
|
||||
for _, payload in payloads do
|
||||
if payload.type == "init" then
|
||||
Charm.batch(applyInitPayload, payload.data)
|
||||
|
|
@ -70,11 +71,12 @@ local function hydrate(payloads: { SyncPayload })
|
|||
Charm.batch(applyPatchPayload, payload.data)
|
||||
end
|
||||
end
|
||||
debug.profileend()
|
||||
end
|
||||
|
||||
return {
|
||||
addSignals = addSignals,
|
||||
removeSignals = removeSignals,
|
||||
removeAllSignals = removeAllSignals,
|
||||
hydrate = hydrate,
|
||||
patch = patch,
|
||||
}
|
||||
|
|
|
|||
6
packages/charm-sync/src/index.d.ts
vendored
6
packages/charm-sync/src/index.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { Atom, Getter, Setter } from "@rbxts/charm";
|
||||
import { Atom, Setter } from "@rbxts/charm";
|
||||
|
||||
type Key = string | number | symbol;
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export interface None {
|
|||
|
||||
export type MaybeNone<T> = undefined extends T ? None : never;
|
||||
|
||||
type GetterMap = Record<string, Getter<any>>;
|
||||
type GetterMap = Record<string, () => any>;
|
||||
type SetterMap = Record<string, Setter<any>>;
|
||||
|
||||
/**
|
||||
|
|
@ -134,7 +134,7 @@ export namespace client {
|
|||
*
|
||||
* @param payloads The state updates received from the server.
|
||||
*/
|
||||
export function hydrate<Getters extends GetterMap = GetterMap, FixArrays extends boolean = true>(
|
||||
export function patch<Getters extends GetterMap = GetterMap, FixArrays extends boolean = true>(
|
||||
payloads: SyncPayload<Getters, FixArrays>[],
|
||||
): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "littensy/charm-sync"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0-rc.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = [
|
||||
|
|
@ -12,4 +12,4 @@ exclude = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
Charm = "littensy/charm@0.11.0-rc.1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/charm",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0-rc.1",
|
||||
"description": "Atomic state management based on alien-signals",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
|
|
|
|||
|
|
@ -367,10 +367,10 @@ end
|
|||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals
|
||||
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): Atom<T>
|
||||
local getter, setter = signal(initialValue, equals)
|
||||
local function atomGetterSetter(...)
|
||||
local function atomOper(...)
|
||||
return if select("#", ...) == 0 then getter() else setter((...))
|
||||
end
|
||||
return atomGetterSetter
|
||||
return atomOper
|
||||
end
|
||||
|
||||
--- Creates a new read-only signal that is computed based on the values of
|
||||
|
|
@ -565,7 +565,7 @@ end
|
|||
--- @return A computed signal that tracks the value returned by the getter.
|
||||
local function toSignal<T>(fn: () -> T): () -> T
|
||||
local name = debug.info(fn, "n")
|
||||
return if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then fn else computed(fn)
|
||||
return if name == "signalGetter" or name == "computedGetter" or name == "atomOper" then fn else computed(fn)
|
||||
end
|
||||
|
||||
--- Creates an effect that runs the callback one immediately, and then again
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "littensy/charm"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0-rc.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/react-charm",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0-rc.1",
|
||||
"description": "React bindings for Charm state management",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "littensy/react-charm"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0-rc.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = [
|
||||
|
|
@ -13,5 +13,5 @@ exclude = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
Charm = "littensy/charm@0.11.0-rc.1"
|
||||
React = "jsdotlua/react@^17"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/vide-charm",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0-rc.1",
|
||||
"description": "Vide bindings for Charm state management",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "littensy/vide-charm"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0-rc.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = [
|
||||
|
|
@ -13,5 +13,5 @@ exclude = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
Charm = "littensy/charm@0.11.0-rc.1"
|
||||
Vide = "centau/vide@^0.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue