mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
Compare commits
147 commits
main
...
charm-v0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea9e83c8b | ||
|
|
c9e42158d6 | ||
|
|
ccdd539b28 | ||
|
|
6499a0064f | ||
|
|
e460846dea | ||
|
|
b00e6760c0 | ||
|
|
71d155e947 | ||
|
|
6a4b2ded3e | ||
|
|
eab89077f6 | ||
|
|
ee6237c4a2 | ||
|
|
1740a440d2 | ||
|
|
ed6dd74512 | ||
|
|
c842926abe | ||
|
|
3134136e70 | ||
|
|
22b440e35f | ||
|
|
748dbc5014 | ||
|
|
c52f55a55e | ||
|
|
27c7f5a6fd | ||
|
|
e3e6209891 | ||
|
|
025d9b2208 | ||
|
|
4a48dacfbd | ||
|
|
1f4c7c7a65 | ||
|
|
890ff2cf07 | ||
|
|
e426e76ff1 | ||
|
|
81d547ae82 | ||
|
|
9d96c1bc05 | ||
|
|
c5753ea71a | ||
|
|
9f7047d484 | ||
|
|
fd6b3efbb5 | ||
|
|
5c27052965 | ||
|
|
50afcaf6e9 | ||
|
|
00880bec55 | ||
|
|
3460618ee0 | ||
|
|
89478f679c | ||
|
|
d741640d28 | ||
|
|
78dbaa476c | ||
|
|
612612b130 | ||
|
|
10521d05c2 | ||
|
|
0d1e85754a | ||
|
|
69d47d580c | ||
|
|
d58d6ffd0b | ||
|
|
6cfdd46a81 | ||
|
|
5aee237f11 | ||
|
|
787084ba59 | ||
|
|
fe7064acee | ||
|
|
fe57482aaf | ||
|
|
5157975091 | ||
|
|
91ef244ef3 | ||
|
|
1ac071b62a | ||
|
|
62fc690a7d | ||
|
|
41d59cdfad | ||
|
|
28872264a2 | ||
|
|
a9658a8aa2 | ||
|
|
a44355b3b6 | ||
|
|
e08204a9d1 | ||
|
|
15b90c5986 | ||
|
|
a20ee3698a | ||
|
|
72b290219d | ||
|
|
fd752d87d7 | ||
|
|
c1b417ec2b | ||
|
|
73954f56e7 | ||
|
|
9d4af8e669 | ||
|
|
b826401bf3 | ||
|
|
b3a76face9 | ||
|
|
aebe3d27ee | ||
|
|
391125b3a2 | ||
|
|
422ee43a94 | ||
|
|
189ae0f168 | ||
|
|
9b89284529 | ||
|
|
1206e008a7 | ||
|
|
366fc668a2 | ||
|
|
9f66803f04 | ||
|
|
f082956ed9 | ||
|
|
b733369e70 | ||
|
|
5c90f928f7 | ||
|
|
bd23d54374 | ||
|
|
7bad215560 | ||
|
|
e583574751 | ||
|
|
ed9566bfff | ||
|
|
29d8add265 | ||
|
|
898fbdba29 | ||
|
|
fdc598f6e6 | ||
|
|
d890f4fc2c | ||
|
|
96ab2317f2 | ||
|
|
dffeb35933 | ||
|
|
7bb6edb403 | ||
|
|
2aa7bd3e15 | ||
|
|
86ade3c36d | ||
|
|
3d4ce41d20 | ||
|
|
90399a816f | ||
|
|
682b3dfcff | ||
|
|
0f2f2d642c | ||
|
|
ea40833d3b | ||
|
|
37f79ecc3f | ||
|
|
7a88c784fa | ||
|
|
32788b19e1 | ||
|
|
1e0b10aef6 | ||
|
|
bf92413a97 | ||
|
|
7a63993293 | ||
|
|
027c5bf8f8 | ||
|
|
87e7260c16 | ||
|
|
1657e2cb74 | ||
|
|
f5709c3112 | ||
|
|
5a05f01223 | ||
|
|
0dbc0587a0 | ||
|
|
079abaacc1 | ||
|
|
4a07297a13 | ||
|
|
cb52ece4d3 | ||
|
|
a8051257e5 | ||
|
|
77c98ca94a | ||
|
|
708bc55de7 | ||
|
|
d2979ebd4c | ||
|
|
95476e93ec | ||
|
|
a9c60c701f | ||
|
|
581488cb4f | ||
|
|
61d9f424aa | ||
|
|
4edc952c05 | ||
|
|
4931515a95 | ||
|
|
25728e589f | ||
|
|
72f936591e | ||
|
|
210e948225 | ||
|
|
546df8cabe | ||
|
|
4e5f656158 | ||
|
|
a53683d59e | ||
|
|
571d2c0d91 | ||
|
|
dcb1de2844 | ||
|
|
3f1556a6e2 | ||
|
|
554010f1bd | ||
|
|
b61d0b3b86 | ||
|
|
f704acce71 | ||
|
|
ada1b80b87 | ||
|
|
d56672384a | ||
|
|
bcf02a615f | ||
|
|
6bfd2f3d21 | ||
|
|
939a812b43 | ||
|
|
1d5d3aed8c | ||
|
|
e276863b2d | ||
|
|
7c48bd2fa4 | ||
|
|
0b37f9cd54 | ||
|
|
d33c4ce891 | ||
|
|
8d385356cd | ||
|
|
3eba56e298 | ||
|
|
8df67d6fff | ||
|
|
209f9b583b | ||
|
|
a0fc88ede1 | ||
|
|
e34b0c9aca | ||
|
|
861ee5acd7 |
28 changed files with 90 additions and 533 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
public-hoist-pattern[]=*@rbxts*
|
||||||
149
README.md
149
README.md
|
|
@ -602,7 +602,7 @@ Charm exposes the following global flags to customize behavior:
|
||||||
| frozen | `true`\* | Enforces data immutability by deep-freezing tables passed to signals, excluding objects with metatables. |
|
| frozen | `true`\* | Enforces data immutability by deep-freezing tables passed to signals, excluding objects with metatables. |
|
||||||
| trackInnerEffects | `true` | Whether nested effects should be tracked and cleaned up when the parent effect re-runs. This should only be disabled to debug issues during migration. |
|
| trackInnerEffects | `true` | Whether nested effects should be tracked and cleaned up when the parent effect re-runs. This should only be disabled to debug issues during migration. |
|
||||||
|
|
||||||
The `strict` and `frozen` flags are automatically enabled in Roblox Studio.
|
The `strict` and `frozen` flags are automatically enabled in Roblox Studio and other development environments where the Luau optimization level is `O1` or lower.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -623,43 +623,34 @@ CharmSync = "littensy/charm-sync@VERSION"
|
||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
|
||||||
Start by specifying the signals that the server should sync to clients. For this example, we'll replicate a round status and timer signal, and each player's inventory:
|
Start by specifying the signals that the server should sync to clients. For this example, we'll use the first and last name signals:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
-- store.luau
|
-- nameStore
|
||||||
|
local getName, setName = signal("John")
|
||||||
local getStatus, setStatus = signal("intermission")
|
local getSurname, setSurname = signal("Doe")
|
||||||
local timerAtom = atom(20)
|
local ageAtom = atom(20)
|
||||||
local getInventories, setInventories = signal({} :: { [Username]: Inventory })
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus = getStatus,
|
getName = getName,
|
||||||
setStatus = setStatus,
|
setName = setName,
|
||||||
timerAtom = timerAtom,
|
getSurname = getSurname,
|
||||||
getInventories = getInventories,
|
setSurname = setSurname,
|
||||||
setInventories = setInventories,
|
ageAtom = ageAtom,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Server setup
|
|
||||||
|
|
||||||
When a player notifies the server that they're ready to start syncing, call `server.addSignalsToClient` with the signals that the client should receive updates for. Once they leave, call `server.removeClient` to unsubscribe them from all updates.
|
When a player notifies the server that they're ready to start syncing, call `server.addSignalsToClient` with the signals that the client should receive updates for. Once they leave, call `server.removeClient` to unsubscribe them from all updates.
|
||||||
|
|
||||||
Then, use `server.connect` to specify how state updates should be sent to each client. Pass a callback function that fires a remote with the given target player and the state updates they subscribed to:
|
Then, use `server.connect` to specify how state updates should be sent to each client. Pass a callback function that fires a remote with the given target player and the state updates they subscribed to:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
-- sync.server.luau
|
|
||||||
|
|
||||||
playerReadyEvent.OnServerEvent:Connect(function(player)
|
playerReadyEvent.OnServerEvent:Connect(function(player)
|
||||||
-- Sync signal getters, computed signals, atoms, or reactive proxies
|
-- Sync signal getters, computed signals, atoms, or reactive proxies
|
||||||
server.addSignalsToClient(player, {
|
server.addSignalsToClient(player, {
|
||||||
status = store.getStatus,
|
name = nameStore.getName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.getSurname,
|
||||||
-- Signals unique to a player should also have a key unique to
|
age = nameStore.ageAtom,
|
||||||
-- that player, to avoid overwriting other synced signals
|
|
||||||
[`inventory-{player.UserId}`] = computed(function()
|
|
||||||
return store.getInventories()[player.Name]
|
|
||||||
end),
|
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -668,54 +659,30 @@ Players.PlayerRemoving:Connect(function(player)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
server.connect(function(player, updates)
|
server.connect(function(player, updates)
|
||||||
-- Customize how you replicate state updates to clients
|
-- Customize how you send state updates to clients
|
||||||
syncEvent:FireClient(player, updates)
|
syncEvent:FireClient(player, updates)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Changes now get replicated to clients
|
|
||||||
store.setStatus("started")
|
|
||||||
store.timerAtom(30)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!WARNING]
|
> [!NOTE]
|
||||||
> On the server, make sure each key corresponds to the same signal across all players. In other words, if you're syncing a signal that is unique to a player, ensure that the key is also unique to that player.
|
> On the server, make sure each key corresponds to the same data across all players. If two players subscribe to the same key, but were given different signals, Charm will output a warning.
|
||||||
>
|
|
||||||
> If two players subscribe to the same key, but were assigned different signals for that key, Charm will output a warning.
|
|
||||||
|
|
||||||
#### Client setup
|
|
||||||
|
|
||||||
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.
|
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.patch` 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
|
```luau
|
||||||
-- sync.client.luau
|
|
||||||
|
|
||||||
-- The server is replicating a signal containing the client's
|
|
||||||
-- inventory, so this signal will receive those updates
|
|
||||||
local getInventory, setInventory = signal({} :: Inventory)
|
|
||||||
|
|
||||||
-- Add signal setters, atoms, or reactive proxies
|
-- Add signal setters, atoms, or reactive proxies
|
||||||
client.addSignals({
|
client.addSignals({
|
||||||
status = store.setStatus,
|
name = nameStore.setName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.setSurname,
|
||||||
-- Signals unique to a player should also have a key unique to
|
age = nameStore.ageAtom,
|
||||||
-- that player, to avoid name collisions
|
|
||||||
[`inventory-{Players.LocalPlayer.UserId}`] = setInventory,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Update client signals when receiving updates from the server
|
-- Update client signals when receiving updates from the server
|
||||||
syncEvent.OnClientEvent:Connect(function(updates)
|
syncEvent.OnClientEvent:Connect(function(updates)
|
||||||
client.patch(updates)
|
client.patch(updates)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Notify the server that the client is ready to receive updates
|
|
||||||
playerReadyEvent:FireServer()
|
|
||||||
|
|
||||||
-- The signals will now update to match the server's state
|
|
||||||
subscribe(getInventory, function(inventory)
|
|
||||||
print("My inventory is", table.concat(inventory))
|
|
||||||
end)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -735,42 +702,38 @@ A configuration table that customizes the behavior of Charm Sync on the server.
|
||||||
|
|
||||||
### `server.addSignalsToClient(client, getters)`
|
### `server.addSignalsToClient(client, getters)`
|
||||||
|
|
||||||
The `addSignalsToClient` function replicates changes in the given signals to a specific client. When an update occurs, the client will receive a delta-compressed state patch containing only the values that changed.
|
The `addSignalsToClient` function subscribes a client to updates in the given signals. When an update occurs, the client will receive a state patch of only the values that changed.
|
||||||
|
|
||||||
You can pass signal getter functions, computed signals, atoms, and [reactive proxies](#reactiveinitialvalue) in the `getters` table. This function can also be called multiple times on the same client to subscribe to new signals.
|
You can pass signal getter functions, computed signals, atoms, and [reactive objects](#reactiveinitialvalue) in the `getters` table. This function can also be called multiple times on the same client to subscribe to new signals.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
playerReadyEvent.OnServerEvent:Connect(function(player)
|
playerReadyEvent.OnServerEvent:Connect(function(player)
|
||||||
server.addSignalsToClient(player, {
|
server.addSignalsToClient(player, {
|
||||||
status = store.getStatus,
|
name = nameStore.getName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.getSurname,
|
||||||
|
age = nameStore.ageAtom,
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
You're also allowed to create new signals to sync to specific players, as long as the key is unique to that signal:
|
You're also allowed to create new signals to sync to specific players, as long as the key is unique to that data:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
playerReadyEvent.OnServerEvent:Connect(function(player)
|
playerReadyEvent.OnServerEvent:Connect(function(player)
|
||||||
server.addSignalsToClient(player, {
|
server.addSignalsToClient(player, {
|
||||||
-- 🟢 Good: Player-specific signals are synced with a unique key
|
-- 🟢 Good: Player-specific data is synced with a unique key
|
||||||
[`playerData-{player.UserId}`] = computed(function()
|
[`data-{player.UserId}`] = computed(function()
|
||||||
return clientStore.getPlayerData(player.UserId)
|
return getPlayerData(player.UserId)
|
||||||
end),
|
end),
|
||||||
|
|
||||||
-- 🔴 Bad: Syncing different signals with the same keys does not work
|
-- 🔴 Bad: Syncing different data with the same keys does not work
|
||||||
playerData = computed(function()
|
playerData = computed(function()
|
||||||
return clientStore.getPlayerData(player.UserId)
|
return getPlayerData(player.UserId)
|
||||||
end),
|
end),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> On the server, make sure each key corresponds to the same signal across all players. In other words, if you're syncing a signal that is unique to a player, ensure that the key is also unique to that player.
|
|
||||||
>
|
|
||||||
> If two players subscribe to the same key, but were assigned different signals for that key, Charm will output a warning.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `server.removeSignalsFromClient(client, ...keys)`
|
### `server.removeSignalsFromClient(client, ...keys)`
|
||||||
|
|
@ -779,11 +742,11 @@ The `removeSignalsFromClient` function unsubscribes the client from a list of ke
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
server.addSignalsToClient(player, {
|
server.addSignalsToClient(player, {
|
||||||
status = store.getStatus,
|
name = nameStore.getName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.getSurname,
|
||||||
})
|
})
|
||||||
|
|
||||||
server.removeSignalsFromClient(player, "status")
|
server.removeSignalsFromClient(player, "surname")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -844,11 +807,9 @@ You can pass either writable signals, atoms, or [reactive objects](#reactiveinit
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
client.addSignals({
|
client.addSignals({
|
||||||
status = store.setStatus,
|
name = nameStore.setName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.setSurname,
|
||||||
-- Signals unique to a player should also have a key unique to
|
age = nameStore.ageAtom,
|
||||||
-- that player, to avoid name collisions
|
|
||||||
[`inventory-{Players.LocalPlayer.UserId}`] = clientStore.setInventory,
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -860,11 +821,11 @@ Unsubscribes from each signal with the corresponding keys. The signals will reta
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
client.addSignals({
|
client.addSignals({
|
||||||
status = store.setStatus,
|
name = nameStore.setName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.setSurname,
|
||||||
})
|
})
|
||||||
|
|
||||||
client.removeSignals("status")
|
client.removeSignals("name")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -877,9 +838,8 @@ You should call `patch` when receiving updates from the server from a remote eve
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
client.addSignals({
|
client.addSignals({
|
||||||
status = store.setStatus,
|
name = nameStore.setName,
|
||||||
timer = store.timerAtom,
|
surname = nameStore.setSurname,
|
||||||
[`inventory-{Players.LocalPlayer.UserId}`] = clientStore.setInventory,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
syncEvent.OnClientEvent:Connect(function(updates)
|
syncEvent.OnClientEvent:Connect(function(updates)
|
||||||
|
|
@ -895,8 +855,8 @@ If you have a lot of signals to sync between the server and clients, it might be
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local sharedState = {
|
local sharedState = {
|
||||||
status = signalToAtom(store.getStatus, store.setStatus),
|
name = signalToAtom(nameStore.getName, nameStore.setName),
|
||||||
inventories = signalToAtom(store.getInventories, store.setInventories),
|
surname = signalToAtom(nameStore.getSurname, nameStore.setSurname),
|
||||||
}
|
}
|
||||||
|
|
||||||
client.addSignals(sharedState)
|
client.addSignals(sharedState)
|
||||||
|
|
@ -909,7 +869,7 @@ server.addSignalsToClient(client, sharedState)
|
||||||
|
|
||||||
Charm Sync will only send clients the differences between the current state and the last-synced state through a process called _delta compression_. In this case, tables are recursively scanned for changes, and unchanged properties are omitted by setting them to `nil`.
|
Charm Sync will only send clients the differences between the current state and the last-synced state through a process called _delta compression_. In this case, tables are recursively scanned for changes, and unchanged properties are omitted by setting them to `nil`.
|
||||||
|
|
||||||
But it's hard to differentiate between an unchanged value and a removed value, as both cases are represented by `nil`. Charm addresses this by representing deleted values with a special `None` value denoted by `{ __none = "__none" }`.
|
But it's hard to differentiate between an unchanged value and a removed value, as both cases are represented by `nil`. We chose to address this by representing deleted values with a special `None` symbol denoted by `{ __none = "__none" }`.
|
||||||
|
|
||||||
This means nilable values may be replaced with `None` in patches, and code working with update payloads (usually for remote argument serialization) should account for nilable values possibly being sent as `None` in the payload.
|
This means nilable values may be replaced with `None` in patches, and code working with update payloads (usually for remote argument serialization) should account for nilable values possibly being sent as `None` in the payload.
|
||||||
|
|
||||||
|
|
@ -925,9 +885,6 @@ You can opt-in to deep reactivity with the Deep Charm library:
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Deep Charm is an experimental library that is not fully documented, and the API is subject to breaking changes.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install @rbxts/deep-charm
|
npm install @rbxts/deep-charm
|
||||||
yarn add @rbxts/deep-charm
|
yarn add @rbxts/deep-charm
|
||||||
|
|
@ -983,22 +940,6 @@ local proxy = reactive(raw)
|
||||||
print(toRaw(proxy) == raw) -- true
|
print(toRaw(proxy) == raw) -- true
|
||||||
```
|
```
|
||||||
|
|
||||||
Calling `toRaw()` on a proxy also subscribes to its changes. This allows you to use proxies with `observe()` and other Charm APIs that accept a getter function:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local proxy, updateProxy = reactive({})
|
|
||||||
|
|
||||||
observe(function()
|
|
||||||
return toRaw(proxy)
|
|
||||||
end, function(value, key)
|
|
||||||
print(`Value {value} added at key {key}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
updateProxy(function(state)
|
|
||||||
table.insert(state, "foo")
|
|
||||||
end) -- Value foo added at key 1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `isReactive(value)`
|
### `isReactive(value)`
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"publish:npm": "pnpm publish -r --access public",
|
"publish:npm": "pnpm publish -r --access public",
|
||||||
"publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally"
|
"publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@12.8.1",
|
"packageManager": "pnpm@10.33.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@rbxts/compiler-types": "3.0.0-types.0",
|
"@rbxts/compiler-types": "3.0.0-types.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rbxts/charm-sync",
|
"name": "@rbxts/charm-sync",
|
||||||
"version": "0.4.1",
|
"version": "0.4.0-rc.7",
|
||||||
"description": "Simple server-to-client sync for Charm",
|
"description": "Simple server-to-client sync for Charm",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/init.luau",
|
"main": "src/init.luau",
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,6 @@ local function applyInitPayload(data: { [string]: any })
|
||||||
trackedSignals[key](value)
|
trackedSignals[key](value)
|
||||||
elseif trackedReactives[key] then
|
elseif trackedReactives[key] then
|
||||||
shallowMergeReactive(trackedReactives[key], value)
|
shallowMergeReactive(trackedReactives[key], value)
|
||||||
else
|
|
||||||
warn(`charm-sync failed to sync key '{key}' because it is not currently registered by addSignals()`)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -82,8 +80,6 @@ local function applyPatchPayload(data: { [string]: any })
|
||||||
elseif trackedReactive then
|
elseif trackedReactive then
|
||||||
patch.applyMutable(trackedReactive.__target, diffs)
|
patch.applyMutable(trackedReactive.__target, diffs)
|
||||||
Charm.trigger(trackedReactive.__track)
|
Charm.trigger(trackedReactive.__track)
|
||||||
else
|
|
||||||
warn(`charm-sync failed to sync key '{key}' because it is not currently registered by addSignals()`)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ local syncCallback: ((client: any, payloads: { SyncPayload }) -> ())?
|
||||||
support custom implementations of reactive proxies.
|
support custom implementations of reactive proxies.
|
||||||
]]
|
]]
|
||||||
local function toRaw<T>(value: T): T
|
local function toRaw<T>(value: T): T
|
||||||
local proxy = value :: types.ReactiveProxy<T>
|
value = value :: types.ReactiveProxy<T>
|
||||||
if type(proxy) == "table" and type(proxy.__target) == "table" and type(proxy.__track) == "function" then
|
if type(value) == "table" and type(value.__target) == "table" and type(value.__track) == "function" then
|
||||||
proxy.__track()
|
value.__track()
|
||||||
return toRaw(proxy.__target)
|
return toRaw(value.__target)
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
@ -61,7 +61,7 @@ local function trackSignal<T>(key: string, getter: () -> T, identifier: unknown)
|
||||||
local rawInitialValue = toRaw(initialValue)
|
local rawInitialValue = toRaw(initialValue)
|
||||||
local snapshot = if initialValue ~= rawInitialValue then utils.deepCopy(rawInitialValue) else initialValue
|
local snapshot = if initialValue ~= rawInitialValue then utils.deepCopy(rawInitialValue) else initialValue
|
||||||
|
|
||||||
local dispose = Charm.untracked(Charm.effect, function()
|
local dispose = Charm.effect(function()
|
||||||
local newValue = getter()
|
local newValue = getter()
|
||||||
local rawValue = toRaw(newValue)
|
local rawValue = toRaw(newValue)
|
||||||
local isReactive = newValue ~= rawValue
|
local isReactive = newValue ~= rawValue
|
||||||
|
|
@ -125,8 +125,8 @@ local function diffGlobalUpdateBuffer(): { any }
|
||||||
local patchCount = #globalUpdates
|
local patchCount = #globalUpdates
|
||||||
local patches = table.create(patchCount)
|
local patches = table.create(patchCount)
|
||||||
|
|
||||||
for index, update in globalUpdates do
|
for _, update in globalUpdates do
|
||||||
patches[index] = patch.diff(update.oldValue, update.newValue)
|
table.insert(patches, patch.diff(update.oldValue, update.newValue))
|
||||||
end
|
end
|
||||||
|
|
||||||
return patches
|
return patches
|
||||||
|
|
@ -146,7 +146,7 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
|
||||||
for index, update in globalUpdates do
|
for index, update in globalUpdates do
|
||||||
local patchData = globalPatches[index]
|
local patchData = globalPatches[index]
|
||||||
|
|
||||||
if patchData == nil then
|
if not patchData then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -403,54 +403,6 @@ test.describe("historical", function()
|
||||||
server.flush()
|
server.flush()
|
||||||
assert(#payloads == 0, "Expected no payloads to be sent for unchanged state")
|
assert(#payloads == 0, "Expected no payloads to be sent for unchanged state")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should maintain patch list order for stale update", function()
|
|
||||||
local a = Charm.atom({ x = 1 })
|
|
||||||
local b = Charm.atom({ y = 1 })
|
|
||||||
local payloads
|
|
||||||
|
|
||||||
server.connect(function(_client, _payloads)
|
|
||||||
payloads = _payloads
|
|
||||||
end)
|
|
||||||
|
|
||||||
server.addSignalsToClient("client", { a = a, b = b })
|
|
||||||
server.flush()
|
|
||||||
|
|
||||||
payloads = nil
|
|
||||||
a({ x = 1 }) -- new table, same contents: diff is nil
|
|
||||||
b({ y = 2 })
|
|
||||||
server.flush()
|
|
||||||
assert(payloads, "Expected payloads to be sent")
|
|
||||||
assert(payloads[1].type == "patch", "Expected payload to be patch")
|
|
||||||
assert(payloads[1].data.b.y == 2, "Expected payload to contain atom 'b'")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("addSignalsToClient should ignore tracking context", function()
|
|
||||||
local counter = Charm.atom(0)
|
|
||||||
local payloads: { any }?
|
|
||||||
|
|
||||||
server.connect(function(_client, _payloads)
|
|
||||||
payloads = _payloads
|
|
||||||
end)
|
|
||||||
|
|
||||||
local dispose = Charm.effectScope(function()
|
|
||||||
server.addSignalsToClient("PLAYER_ID", { counter = counter })
|
|
||||||
end)
|
|
||||||
|
|
||||||
server.flush()
|
|
||||||
assert(payloads, "Expected payloads to be sent")
|
|
||||||
assert(payloads[1].type == "init", "Expected first payload to be init")
|
|
||||||
assert(payloads[1].data.counter == 0, "Expected init payload to contain atom 'counter'")
|
|
||||||
|
|
||||||
dispose()
|
|
||||||
|
|
||||||
payloads = nil
|
|
||||||
counter(1)
|
|
||||||
server.flush()
|
|
||||||
assert(payloads, "Expected payloads to be sent")
|
|
||||||
assert(payloads[1].type == "patch", "Expected first payload to be patch")
|
|
||||||
assert(payloads[1].data.counter == 1, "Expected patch payload to contain atom 'counter'")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ test("should flatten functions", function()
|
||||||
local input = {
|
local input = {
|
||||||
a = Charm.atom(0),
|
a = Charm.atom(0),
|
||||||
b = { c = Charm.atom(0) },
|
b = { c = Charm.atom(0) },
|
||||||
d = { e = { f = utils.signalToAtom<<number>>(Charm.signal(0)) } },
|
d = { e = { f = utils.signalToAtom(Charm.signal(0)) } },
|
||||||
}
|
}
|
||||||
|
|
||||||
local result = utils.flatten<<{ [string]: Charm.Atom<number> }>>(input)
|
local result = utils.flatten<<{ [string]: Charm.Atom<number> }>>(input)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "littensy/charm-sync"
|
name = "littensy/charm-sync"
|
||||||
version = "0.4.1"
|
version = "0.4.0-rc.7"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|
@ -12,4 +12,4 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
Charm = "littensy/charm@^0.11.0"
|
Charm = "littensy/charm@^0.11.0-rc.6"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rbxts/charm",
|
"name": "@rbxts/charm",
|
||||||
"version": "0.11.1",
|
"version": "0.11.0-rc.6",
|
||||||
"description": "Atomic state management based on alien-signals",
|
"description": "Atomic state management based on alien-signals",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/init.luau",
|
"main": "src/init.luau",
|
||||||
|
|
|
||||||
4
packages/charm/src/index.d.ts
vendored
4
packages/charm/src/index.d.ts
vendored
|
|
@ -50,12 +50,12 @@ export const flags: {
|
||||||
/**
|
/**
|
||||||
* Enforces synchronous, non-yielding behavior in signals and effects.
|
* Enforces synchronous, non-yielding behavior in signals and effects.
|
||||||
* Also enables state validation in Charm Sync to catch sync errors early.
|
* Also enables state validation in Charm Sync to catch sync errors early.
|
||||||
* Enabled by default in Roblox Studio.
|
* Enabled if the optimization level is below 2, which is true in studio.
|
||||||
*/
|
*/
|
||||||
strict: boolean;
|
strict: boolean;
|
||||||
/**
|
/**
|
||||||
* Enforces immutability of tables by deep-freezing table values. Enabled
|
* Enforces immutability of tables by deep-freezing table values. Enabled
|
||||||
* by default in Roblox Studio.
|
* if the optimization level is below 2, which is true in studio.
|
||||||
*/
|
*/
|
||||||
frozen: boolean;
|
frozen: boolean;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -68,22 +68,22 @@ local batchDepth = 0
|
||||||
-- Dependencies accessed during this time will link to the active subscriber.
|
-- Dependencies accessed during this time will link to the active subscriber.
|
||||||
local activeSub: ReactiveNode?
|
local activeSub: ReactiveNode?
|
||||||
|
|
||||||
local function isStudio(): boolean
|
local function isO2(): boolean
|
||||||
return game ~= nil and game:GetService("RunService"):IsStudio()
|
return debug.info(1, "n") ~= "isO2"
|
||||||
end
|
end
|
||||||
|
|
||||||
local flags = {
|
local flags = {
|
||||||
--[[
|
--[[
|
||||||
Enforces synchronous, non-yielding behavior in signals and effects.
|
Enforces synchronous, non-yielding behavior in signals and effects.
|
||||||
Also enables state validation in Charm Sync to catch sync errors early.
|
Also enables state validation in Charm Sync to catch sync errors early.
|
||||||
Enabled by default in Roblox Studio.
|
Enabled if the optimization level is below 2, which is true in studio.
|
||||||
]]
|
]]
|
||||||
strict = isStudio(),
|
strict = not isO2(),
|
||||||
--[[
|
--[[
|
||||||
Enforces immutability of tables by deep-freezing table values. Enabled
|
Enforces immutability of tables by deep-freezing table values. Enabled
|
||||||
by default in Roblox Studio.
|
if the optimization level is below 2, which is true in studio.
|
||||||
]]
|
]]
|
||||||
frozen = isStudio(),
|
frozen = not isO2(),
|
||||||
--[[
|
--[[
|
||||||
Whether parent effects should track inner effects and clean them up
|
Whether parent effects should track inner effects and clean them up
|
||||||
when the parent effect re-runs. This is usually desirable, but can be
|
when the parent effect re-runs. This is usually desirable, but can be
|
||||||
|
|
@ -486,7 +486,7 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if the new value is different from the pending value
|
-- Check if the new value is different from the pending value
|
||||||
if if equals then not equals(node.pendingValue, value :: T) else node.pendingValue ~= value then
|
if if equals then not equals(node.pendingValue, value) else node.pendingValue ~= value then
|
||||||
if flags.frozen then
|
if flags.frozen then
|
||||||
deepFreeze(value)
|
deepFreeze(value)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -198,23 +198,4 @@ test("should trigger effect even computed already dirty", function()
|
||||||
assert(v() == 2, `expected 2 for v(), got {v()}`)
|
assert(v() == 2, `expected 2 for v(), got {v()}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should handle inner effect clearing outer computed's deps on rerun", function()
|
|
||||||
local src, setSrc = signal(false)
|
|
||||||
local stopEffect: () -> ()
|
|
||||||
local c = computed(function()
|
|
||||||
src()
|
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
stopEffect()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
effect(function() end)
|
|
||||||
return 0
|
|
||||||
end)
|
|
||||||
stopEffect = effect(function()
|
|
||||||
c()
|
|
||||||
end)
|
|
||||||
setSrc(true)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -504,90 +504,4 @@ test("recursive effects should run and cleanup in order", function()
|
||||||
assert(table.concat(order) == "-3+100", `bad order: {table.concat(order)}`)
|
assert(table.concat(order) == "-3+100", `bad order: {table.concat(order)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should effects dispose in order", function()
|
|
||||||
local order: { number } = {}
|
|
||||||
|
|
||||||
local src, setSrc = signal(false)
|
|
||||||
local stopEffect = effect(function()
|
|
||||||
src()
|
|
||||||
for i = 1, 3 do
|
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
table.insert(order, i)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert(#order == 0, "should not cleanup immediately")
|
|
||||||
setSrc(true)
|
|
||||||
assert(table.concat(order) == "123", `bad rerun order: {table.concat(order)}`)
|
|
||||||
table.clear(order)
|
|
||||||
stopEffect()
|
|
||||||
assert(table.concat(order) == "123", `bad dispose order: {table.concat(order)}`)
|
|
||||||
table.clear(order)
|
|
||||||
setSrc(false)
|
|
||||||
assert(#order == 0, "should not cleanup after dispose")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should handle inner effect clearing outer effect's deps on rerun", function()
|
|
||||||
local src, setSrc = signal(false)
|
|
||||||
local stopEffect: () -> ()
|
|
||||||
stopEffect = effect(function()
|
|
||||||
src()
|
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
stopEffect()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
effect(function() end)
|
|
||||||
end)
|
|
||||||
setSrc(true)
|
|
||||||
end)
|
|
||||||
|
|
||||||
test.skip("should handle inner effect cleanup disposing outer effect", function()
|
|
||||||
local order: { number } = {}
|
|
||||||
|
|
||||||
local src, setSrc = signal(false)
|
|
||||||
local stopEffect: () -> ()
|
|
||||||
stopEffect = effect(function()
|
|
||||||
src()
|
|
||||||
table.insert(order, 0)
|
|
||||||
for i = 1, 4 do
|
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
table.insert(order, i)
|
|
||||||
stopEffect()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
assert(table.concat(order) == "0", "should not cleanup immediately")
|
|
||||||
|
|
||||||
table.clear(order)
|
|
||||||
setSrc(true)
|
|
||||||
assert(table.concat(order) == "1234", `bad rerun order: {table.concat(order)}`)
|
|
||||||
|
|
||||||
table.clear(order)
|
|
||||||
setSrc(false)
|
|
||||||
assert(#order == 0, "should not cleanup after dispose")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test.skip("should old inner effect dispose before outer effect re-runs", function()
|
|
||||||
local order: { string } = {}
|
|
||||||
local src, setSrc = signal(false)
|
|
||||||
effect(function()
|
|
||||||
src()
|
|
||||||
table.insert(order, "outer")
|
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
table.insert(order, "inner")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
table.clear(order)
|
|
||||||
setSrc(true)
|
|
||||||
assert(table.concat(order) == "innerouter", `bad rerun order: {table.concat(order)}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ test("should observe items", function()
|
||||||
local updates = {}
|
local updates = {}
|
||||||
local cleanups = {}
|
local cleanups = {}
|
||||||
|
|
||||||
observe(items, function(_, key: number)
|
observe(items, function(_, key)
|
||||||
local get = computed(function()
|
local get = computed(function()
|
||||||
return items()[key]
|
return items()[key]
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "littensy/charm"
|
name = "littensy/charm"
|
||||||
version = "0.11.1"
|
version = "0.11.0-rc.6"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
exclude = ["node_modules", "test", "src/index.d.ts", "package.json"]
|
exclude = ["node_modules", "test", "src/index.d.ts", "package.json"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rbxts/deep-charm",
|
"name": "@rbxts/deep-charm",
|
||||||
"version": "0.1.1",
|
"version": "0.1.0-rc.1",
|
||||||
"description": "Use Charm with plain Luau tables",
|
"description": "Use Charm with plain Luau tables",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/init.luau",
|
"main": "src/init.luau",
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ end
|
||||||
]]
|
]]
|
||||||
local function toRaw<T>(value: T): T
|
local function toRaw<T>(value: T): T
|
||||||
if isReactive(value) then
|
if isReactive(value) then
|
||||||
local proxy = value :: ReactiveProxy<T>
|
value = value :: ReactiveProxy<T>
|
||||||
proxy.__track()
|
value.__track()
|
||||||
return toRaw(proxy.__target)
|
return toRaw(value.__target)
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
@ -115,7 +115,7 @@ end
|
||||||
@return A reactive proxy if the value is a table
|
@return A reactive proxy if the value is a table
|
||||||
]]
|
]]
|
||||||
local function toReactive<T>(value: T, trackParent: (() -> ())?): T
|
local function toReactive<T>(value: T, trackParent: (() -> ())?): T
|
||||||
return if type(value) == "table" then reactive(value, trackParent) :: T else value :: T
|
return if type(value) == "table" then reactive(value, trackParent) else value
|
||||||
end
|
end
|
||||||
|
|
||||||
function ReactiveProxy.__index(self: ReactiveProxy, key: any): any
|
function ReactiveProxy.__index(self: ReactiveProxy, key: any): any
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ local Charm = require("../Charm")
|
||||||
local DeepCharm = require("../src")
|
local DeepCharm = require("../src")
|
||||||
local test = require("../../../tests/test")
|
local test = require("../../../tests/test")
|
||||||
|
|
||||||
local batch = Charm.batch
|
|
||||||
local computed = Charm.computed
|
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local observe = Charm.observe
|
|
||||||
local reactive = DeepCharm.reactive
|
local reactive = DeepCharm.reactive
|
||||||
local toRaw = DeepCharm.toRaw
|
local toRaw = DeepCharm.toRaw
|
||||||
|
|
||||||
|
|
@ -189,44 +186,4 @@ test("should unwrap nested proxy", function()
|
||||||
assert(runs == 3, `bad runs: {runs}`)
|
assert(runs == 3, `bad runs: {runs}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should observe items", function()
|
|
||||||
local items, updateItems = reactive({ 0, 0, 0 })
|
|
||||||
local updates = {}
|
|
||||||
local cleanups = {}
|
|
||||||
|
|
||||||
observe(function()
|
|
||||||
return toRaw(items)
|
|
||||||
end, function(_, key: number)
|
|
||||||
local get = computed(function()
|
|
||||||
return items[key]
|
|
||||||
end)
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
table.insert(updates, get())
|
|
||||||
end)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
table.insert(cleanups, key)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert(table.concat(updates) == "000", `bad mount: {table.concat(updates)}`)
|
|
||||||
|
|
||||||
table.clear(updates)
|
|
||||||
updateItems(function(state)
|
|
||||||
state[1] = 1
|
|
||||||
state[2] = 2
|
|
||||||
state[3] = 3
|
|
||||||
end)
|
|
||||||
assert(table.concat(updates) == "123", `bad update: {table.concat(updates)}`)
|
|
||||||
|
|
||||||
table.clear(updates)
|
|
||||||
batch(function()
|
|
||||||
items[3] = nil
|
|
||||||
items[4] = 4
|
|
||||||
end)
|
|
||||||
assert(table.concat(updates) == "4", `should add 4: {table.concat(updates)}`)
|
|
||||||
assert(table.concat(cleanups) == "3", `bad cleanup: {table.concat(cleanups)}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "littensy/deep-charm"
|
name = "littensy/deep-charm"
|
||||||
version = "0.1.1"
|
version = "0.1.0-rc.1"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
exclude = ["node_modules", "test", "src/index.d.ts", "package.json"]
|
exclude = ["node_modules", "test", "src/index.d.ts", "package.json"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
Charm = "littensy/charm@^0.11.0"
|
Charm = "littensy/charm@^0.11.0-rc.6"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rbxts/react-charm",
|
"name": "@rbxts/react-charm",
|
||||||
"version": "0.4.1",
|
"version": "0.4.0-rc.4",
|
||||||
"description": "React bindings for Charm state management",
|
"description": "React bindings for Charm state management",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/init.luau",
|
"main": "src/init.luau",
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ local React = require("./React")
|
||||||
@return The current state.
|
@return The current state.
|
||||||
]]
|
]]
|
||||||
local function useSignalState<T>(getter: () -> T, dependencies: {}?): T
|
local function useSignalState<T>(getter: () -> T, dependencies: {}?): T
|
||||||
local value: T, setValue = React.useState(getter)
|
local value, setValue = React.useState(getter)
|
||||||
|
|
||||||
React.useLayoutEffect(function(): () -> ()
|
React.useLayoutEffect(function()
|
||||||
return Charm.listen(getter, setValue :: (T, T?) -> ())
|
return Charm.listen(getter, setValue :: (T, T?) -> ())
|
||||||
end, dependencies :: { any } or {})
|
end, dependencies :: { any } or {})
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ local function useSignalBinding<T>(getter: () -> T, dependencies: {}?)
|
||||||
local initialValue = React.useMemo(getter, {})
|
local initialValue = React.useMemo(getter, {})
|
||||||
local binding, setBinding = React.useBinding(initialValue)
|
local binding, setBinding = React.useBinding(initialValue)
|
||||||
|
|
||||||
React.useLayoutEffect(function(): () -> ()
|
React.useLayoutEffect(function()
|
||||||
return Charm.listen(getter, setBinding :: (T, T?) -> ())
|
return Charm.listen(getter, setBinding :: (T, T?) -> ())
|
||||||
end, dependencies :: { any } or {})
|
end, dependencies :: { any } or {})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "littensy/react-charm"
|
name = "littensy/react-charm"
|
||||||
version = "0.4.1"
|
version = "0.4.0-rc.4"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|
@ -13,5 +13,5 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
Charm = "littensy/charm@^0.11.0"
|
Charm = "littensy/charm@^0.11.0-rc.6"
|
||||||
React = "jsdotlua/react@^17"
|
React = "jsdotlua/react@^17"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@rbxts/vide-charm",
|
"name": "@rbxts/vide-charm",
|
||||||
"version": "0.4.1",
|
"version": "0.4.0-rc.4",
|
||||||
"description": "Vide bindings for Charm state management",
|
"description": "Vide bindings for Charm state management",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/init.luau",
|
"main": "src/init.luau",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "littensy/vide-charm"
|
name = "littensy/vide-charm"
|
||||||
version = "0.4.1"
|
version = "0.4.0-rc.5"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|
@ -13,5 +13,5 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
Charm = "littensy/charm@^0.11.0"
|
Charm = "littensy/charm@^0.11.0-rc.6"
|
||||||
Vide = "centau/vide@^0.4"
|
Vide = "centau/vide@^0.4"
|
||||||
|
|
|
||||||
183
pnpm-lock.yaml
generated
183
pnpm-lock.yaml
generated
|
|
@ -1,186 +1,3 @@
|
||||||
---
|
|
||||||
lockfileVersion: '9.0'
|
|
||||||
|
|
||||||
importers:
|
|
||||||
|
|
||||||
.:
|
|
||||||
configDependencies: {}
|
|
||||||
packageManagerDependencies:
|
|
||||||
'@pnpm/exe':
|
|
||||||
specifier: 12.8.1
|
|
||||||
version: 12.8.1
|
|
||||||
pnpm:
|
|
||||||
specifier: 12.8.1
|
|
||||||
version: 12.8.1
|
|
||||||
|
|
||||||
packages:
|
|
||||||
|
|
||||||
'@pnpm/exe.android-arm64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@pnpm/exe.android-x64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@pnpm/exe.darwin-arm64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@pnpm/exe.darwin-x64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@pnpm/exe.freebsd-x64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-arm64-musl@12.8.1':
|
|
||||||
resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-arm64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-ppc64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==}
|
|
||||||
cpu: [ppc64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-riscv64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==}
|
|
||||||
cpu: [riscv64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-s390x@12.8.1':
|
|
||||||
resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==}
|
|
||||||
cpu: [s390x]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-x64-musl@12.8.1':
|
|
||||||
resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-x64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@pnpm/exe.win32-arm64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@pnpm/exe.win32-x64@12.8.1':
|
|
||||||
resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@pnpm/exe@12.8.1':
|
|
||||||
resolution: {integrity: sha512-ju5MeefhEEXI7NO3u6oj54uGBOkN0pVhI+tcmuZDkTTqVdRZvEVEo+zhjwAoe8vEXguQCvsa6cTDhmFPxpOXeQ==}
|
|
||||||
engines: {node: '>=18.*'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
pnpm@12.8.1:
|
|
||||||
resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==}
|
|
||||||
engines: {node: '>=18.*'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
snapshots:
|
|
||||||
|
|
||||||
'@pnpm/exe.android-arm64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.android-x64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.darwin-arm64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.darwin-x64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.freebsd-x64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-arm64-musl@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-arm64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-ppc64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-riscv64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-s390x@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-x64-musl@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.linux-x64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.win32-arm64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe.win32-x64@12.8.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@pnpm/exe@12.8.1':
|
|
||||||
optionalDependencies:
|
|
||||||
'@pnpm/exe.android-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.android-x64': 12.8.1
|
|
||||||
'@pnpm/exe.darwin-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.darwin-x64': 12.8.1
|
|
||||||
'@pnpm/exe.freebsd-x64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-arm64-musl': 12.8.1
|
|
||||||
'@pnpm/exe.linux-ppc64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-riscv64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-s390x': 12.8.1
|
|
||||||
'@pnpm/exe.linux-x64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-x64-musl': 12.8.1
|
|
||||||
'@pnpm/exe.win32-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.win32-x64': 12.8.1
|
|
||||||
|
|
||||||
pnpm@12.8.1:
|
|
||||||
optionalDependencies:
|
|
||||||
'@pnpm/exe.android-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.android-x64': 12.8.1
|
|
||||||
'@pnpm/exe.darwin-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.darwin-x64': 12.8.1
|
|
||||||
'@pnpm/exe.freebsd-x64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-arm64-musl': 12.8.1
|
|
||||||
'@pnpm/exe.linux-ppc64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-riscv64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-s390x': 12.8.1
|
|
||||||
'@pnpm/exe.linux-x64': 12.8.1
|
|
||||||
'@pnpm/exe.linux-x64-musl': 12.8.1
|
|
||||||
'@pnpm/exe.win32-arm64': 12.8.1
|
|
||||||
'@pnpm/exe.win32-x64': 12.8.1
|
|
||||||
|
|
||||||
---
|
|
||||||
lockfileVersion: '9.0'
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
packages:
|
packages:
|
||||||
- packages/*
|
- packages/*
|
||||||
publicHoistPattern:
|
|
||||||
- "*@rbxts*"
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tools]
|
[tools]
|
||||||
wally = "UpliftGames/wally@0.3.2"
|
wally = "UpliftGames/wally@0.3.2"
|
||||||
rojo = "rojo-rbx/rojo@7.7.0"
|
rojo = "rojo-rbx/rojo@7.6.1"
|
||||||
stylua = "johnnymorganz/stylua@2.5.2"
|
stylua = "johnnymorganz/stylua@2.4.1"
|
||||||
luau-lsp = "johnnymorganz/luau-lsp@1.70.1"
|
luau-lsp = "johnnymorganz/luau-lsp@1.66.1"
|
||||||
lute = "luau-lang/lute@1.0.0"
|
lute = "luau-lang/lute@1.0.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue