Compare commits

..

147 commits

Author SHA1 Message Date
littensy
aea9e83c8b release: charm v0.11.0-rc.6 2026-05-05 22:14:53 -07:00
littensy
c9e42158d6 test: add multi benchmark 2026-05-05 21:24:04 -07:00
littensy
ccdd539b28 fix: always return pending value in signal setter 2026-05-05 21:23:45 -07:00
littensy
6499a0064f ci: remove selene 2026-05-05 21:17:51 -07:00
littensy
e460846dea refactor: check type of cleanup 2026-04-25 22:35:35 -07:00
littensy
b00e6760c0 perf: remove clearPending inline operation 2026-04-25 22:32:53 -07:00
littensy
71d155e947 chore: housekeeping 2026-04-19 18:46:17 -07:00
littensy
6a4b2ded3e docs: link roblox signals 2026-04-11 13:18:18 -07:00
littensy
eab89077f6 refactor: improve error traceback 2026-04-11 13:06:53 -07:00
littensy
ee6237c4a2 refactor: satisfy new solver 2026-04-11 11:13:31 -07:00
littensy
1740a440d2 chore: housekeeping 2026-04-08 06:50:39 -07:00
littensy
ed6dd74512 refactor: improve errors in nonstrict, optimize noyield 2026-04-05 03:17:27 -07:00
littensy
c842926abe chore: housekeeping 2026-04-03 07:26:22 -07:00
littensy
3134136e70 ci: disable new solver flag until 0.714 2026-04-02 07:08:08 -07:00
littensy
22b440e35f refactor: use new type solver 2026-04-02 06:49:54 -07:00
littensy
748dbc5014 refactor: error for effect cleanup errors 2026-04-02 05:17:51 -07:00
littensy
c52f55a55e perf: call mapper as-is in mapped() 2026-04-01 05:29:55 -07:00
littensy
27c7f5a6fd perf: run observe fn in untracked 2026-04-01 04:37:50 -07:00
littensy
e3e6209891 ci: check eslint and prettier 2026-04-01 04:19:04 -07:00
littensy
025d9b2208 refactor!: run effects in batched 2026-04-01 04:16:16 -07:00
littensy
4a48dacfbd fix: check if cleanup functions disposed effect 2026-03-30 04:11:57 -07:00
littensy
1f4c7c7a65 refactor: keep error level in some cases 2026-03-30 03:14:33 -07:00
littensy
890ff2cf07 refactor: shorten error traceback, housekeeping 2026-03-29 15:25:33 -07:00
littensy
e426e76ff1 fix: remove old keys when updating key in mapped() 2026-03-27 04:39:02 -07:00
littensy
81d547ae82 release: react-charm v0.4.0-rc.4 2026-03-26 01:11:08 -07:00
littensy
9d96c1bc05 release: charm-sync v0.4.0-rc.7 2026-03-26 01:07:58 -07:00
littensy
c5753ea71a release: charm v0.11.0-rc.5 2026-03-26 01:01:12 -07:00
littensy
9f7047d484 docs: note peek in migration guide 2026-03-22 13:09:29 -07:00
littensy
fd6b3efbb5 refactor!: remove peek 2026-03-22 13:08:28 -07:00
littensy
5c27052965 feat!: run listen and subscribe callback in untracked 2026-03-19 18:42:30 -07:00
littensy
50afcaf6e9 docs: rephrase intro again 2026-03-15 12:38:46 -07:00
littensy
00880bec55 feat: export getter and setter types 2026-03-07 19:32:23 -08:00
littensy
3460618ee0 docs: rephrase some sections 2026-03-02 11:31:17 -08:00
littensy
89478f679c refactor: reorganize repo, create deep-charm 2026-02-24 19:49:34 -08:00
littensy
d741640d28 refactor: remove recursive() 2026-02-22 23:21:57 -08:00
littensy
78dbaa476c release: charm v0.11.0-rc.4 2026-02-22 23:19:28 -08:00
littensy
612612b130 feat: allow recursion in effects by default 2026-02-22 23:14:50 -08:00
littensy
10521d05c2 refactor: move test directories 2026-02-19 22:31:40 -08:00
littensy
0d1e85754a ci: ignore eslint config file 2026-02-19 22:15:05 -08:00
littensy
69d47d580c ci: use javascript eslint config 2026-02-19 21:55:11 -08:00
littensy
d58d6ffd0b chore: update lockfile 2026-02-19 21:47:28 -08:00
littensy
6cfdd46a81 build: fix react peer dep for ci 2026-02-19 21:46:37 -08:00
littensy
5aee237f11 docs: move listen nesting note 2026-02-17 18:46:30 -08:00
littensy
787084ba59 docs: adjust ordering by importance 2026-02-17 18:45:00 -08:00
littensy
fe7064acee docs: improve intro and explain getters 2026-02-17 18:38:30 -08:00
littensy
fe57482aaf chore: update vide 2026-02-17 17:47:29 -08:00
littensy
5157975091 chore: fix react tests, run stylua 2026-02-17 17:47:05 -08:00
littensy
91ef244ef3 test: add important vue reactivity tests 2026-02-17 17:31:14 -08:00
littensy
1ac071b62a fix: add missing recursed check 2026-02-17 17:30:51 -08:00
safazi
62fc690a7d
fix: update signal overload for setter (#58) 2026-02-15 21:06:55 -08:00
littensy
41d59cdfad chore: bump deps 2026-01-12 17:24:36 -08:00
littensy
28872264a2 build: loosen workspace version requirements 2025-12-14 03:34:05 -08:00
littensy
a9658a8aa2 ci: upgrade lute 2025-12-14 03:03:10 -08:00
littensy
a44355b3b6 refactor: improve source readability 2025-12-11 09:26:21 -08:00
littensy
e08204a9d1 ci: set up lute 2025-12-01 20:12:28 -08:00
littensy
15b90c5986 chore: bump versions 2025-12-01 17:30:54 -08:00
littensy
a20ee3698a test: use lute for tests 2025-12-01 17:29:39 -08:00
littensy
72b290219d fix: remove unsynced signals immediately 2025-12-01 17:29:04 -08:00
littensy
fd752d87d7 chore: bump release candidate 2025-11-29 18:45:47 -08:00
littensy
c1b417ec2b fix: loosen client signal types 2025-11-29 15:50:07 -08:00
littensy
73954f56e7 fix: wrap iterated values in reactive() 2025-11-28 21:09:46 -08:00
littensy
9d4af8e669 fix: prevent early reactive gc, alien-signals 3.1.1 2025-11-28 20:36:51 -08:00
littensy
b826401bf3 fix: refine reactive proxy logic 2025-11-28 01:18:57 -08:00
littensy
b3a76face9 feat: sync signals that return reactive proxy 2025-11-25 00:29:08 -08:00
littensy
aebe3d27ee docs: add fishing game example project 2025-11-23 16:28:11 -08:00
littensy
391125b3a2 chore: bump charm-sync version 2025-11-22 22:04:27 -08:00
littensy
422ee43a94 fix: check none before patching mutable tables 2025-11-22 22:03:42 -08:00
littensy
189ae0f168 fix: make toRaw track all calls 2025-11-21 22:15:08 -08:00
littensy
9b89284529 perf: do not create proxies for iteration 2025-11-21 19:50:37 -08:00
littensy
1206e008a7 chore: bump versions 2025-11-20 15:06:30 -08:00
littensy
366fc668a2 fix: check for nil diffs 2025-11-20 15:05:59 -08:00
littensy
9f66803f04 feat: add toRaw() 2025-11-19 21:38:56 -08:00
littensy
f082956ed9 docs: fix typo 2025-11-18 20:50:48 -08:00
littensy
b733369e70 docs: reword reactive docs 2025-11-18 20:50:28 -08:00
littensy
5c90f928f7 chore: bump versions 2025-11-18 20:40:38 -08:00
littensy
bd23d54374 feat: add reactive() 2025-11-18 20:39:37 -08:00
richard
7bad215560
docs: fix typo 2025-11-16 07:02:38 -08:00
littensy
e583574751 test: make some tests more reliable 2025-11-15 20:41:38 -08:00
littensy
ed9566bfff chore: prepare release candidate 2025-11-15 20:38:15 -08:00
littensy
29d8add265 docs: update readme 2025-11-12 16:43:49 -08:00
littensy
898fbdba29 feat: add trigger(), refactor source 2025-11-12 16:28:41 -08:00
littensy
fdc598f6e6 fix: allow dispose in observers 2025-11-05 12:35:25 -08:00
littensy
d890f4fc2c feat: add recursive() 2025-11-05 02:30:41 -08:00
littensy
96ab2317f2 feat: update alien-signals, add recursion flag 2025-11-05 00:16:18 -08:00
littensy
dffeb35933 refactor: simplify charm sync validation 2025-11-04 22:30:10 -08:00
littensy
7bb6edb403 refactor: rename batch, misc fixes & perf updates 2025-11-04 21:39:28 -08:00
littensy
2aa7bd3e15 docs: rephrase computed arg change 2025-11-03 02:22:51 -08:00
littensy
86ade3c36d docs: fix whitespace 2025-11-03 02:16:45 -08:00
littensy
3d4ce41d20 docs: add migration tips 2025-11-03 02:15:15 -08:00
littensy
90399a816f docs: capitalize headers 2025-11-03 00:51:34 -08:00
littensy
682b3dfcff docs: simplify examples 2025-11-03 00:47:35 -08:00
littensy
0f2f2d642c docs: update sync section 2025-11-03 00:41:24 -08:00
littensy
ea40833d3b feat: finalize sync docs 2025-11-03 00:12:06 -08:00
littensy
37f79ecc3f refactor: remove unnecessary features, update docs 2025-11-02 18:55:41 -08:00
littensy
7a88c784fa feat: update alien-signals 2025-10-31 23:06:43 -07:00
littensy
32788b19e1 refactor: update charm sync api 2025-10-31 21:56:02 -07:00
littensy
1e0b10aef6 feat: add signal api 2025-10-31 00:36:55 -07:00
littensy
bf92413a97 refactor: align with alien-signals 3.0 2025-10-18 21:59:34 -07:00
littensy
7a63993293 refactor: add docs, remove some modules 2025-10-03 20:45:01 -07:00
littensy
027c5bf8f8 feat: add flag for inner effect tracking 2025-10-02 19:37:03 -07:00
littensy
87e7260c16 fix: wrap hooks in untracked, update types 2025-10-02 19:30:40 -07:00
littensy
1657e2cb74 feat: add peek, make untracked escape scope 2025-10-02 19:29:04 -07:00
littensy
f5709c3112 feat: prepare for alpha release 2025-10-02 01:43:28 -07:00
littensy
5a05f01223 test: add all sync tests 2025-10-01 01:30:31 -07:00
littensy
0dbc0587a0 fix: send init payload even if atom did not update 2025-09-29 18:30:35 -07:00
littensy
079abaacc1 refactor: start charm-sync rewrite 2025-09-29 18:11:39 -07:00
littensy
4a07297a13 chore: update eslint, housekeeping 2025-09-25 15:17:08 -07:00
littensy
cb52ece4d3 refactor: rewrite untracked, update alien-signals 2025-09-23 16:50:39 -07:00
littensy
a8051257e5 refactor: reduce type casting, housekeeping 2025-07-20 20:52:17 -07:00
littensy
77c98ca94a feat: add deep freeze flag, misc refactors 2025-07-05 13:38:18 -07:00
littensy
708bc55de7 feat: add peek and roblox-ts types 2025-07-02 21:58:55 -07:00
littensy
d2979ebd4c ci: update test file path 2025-07-02 09:28:38 -07:00
littensy
95476e93ec refactor: improve structure and alien-signals integration 2025-07-02 09:25:50 -07:00
littensy
a9c60c701f test: use lune 2025-05-21 15:42:48 -07:00
littensy
581488cb4f ci: init submodules 2025-05-21 12:24:53 -07:00
littensy
61d9f424aa ci: add pnpm step 2025-05-21 12:22:00 -07:00
littensy
4edc952c05 chore: housekeeping 2025-05-21 12:16:29 -07:00
littensy
4931515a95 refactor: improve logic 2025-05-12 19:26:15 -07:00
littensy
25728e589f style: use --- doc comments 2025-05-12 02:20:41 -07:00
littensy
72f936591e refactor: improve logic 2025-05-12 02:15:49 -07:00
littensy
210e948225 refactor: iterate over previous data in mapped 2025-05-11 17:49:01 -07:00
littensy
546df8cabe test: separate parent from root 2025-05-11 14:45:24 -07:00
littensy
4e5f656158 test: improve test suite 2025-05-11 04:56:46 -07:00
littensy
a53683d59e refactor: use setCurrentSub instead of pauseTracking 2025-05-08 00:12:51 -07:00
littensy
571d2c0d91 refactor: sync 2.0.4 2025-05-08 00:04:00 -07:00
littensy
dcb1de2844 test: color brackets 2025-05-06 19:40:24 -07:00
littensy
3f1556a6e2 test: try different gray 2025-05-06 19:37:54 -07:00
littensy
554010f1bd test: prettify test library 2025-05-06 19:32:19 -07:00
littensy
b61d0b3b86 perf: simplify mapped logic 2025-05-06 17:07:30 -07:00
littensy
f704acce71 chore: delete redundant benchmarks 2025-05-06 16:27:50 -07:00
littensy
ada1b80b87 perf: optimize mapped, add benches 2025-05-06 16:27:06 -07:00
littensy
d56672384a refactor: add assertion error messages 2025-05-06 11:18:19 -07:00
littensy
bcf02a615f refactor: avoid ambiguous optional function type 2025-05-06 11:06:27 -07:00
littensy
6bfd2f3d21 perf: optimize subscribe and mapped 2025-05-06 11:04:39 -07:00
littensy
939a812b43 chore: rephrase doc comment 2025-05-06 02:30:33 -07:00
littensy
1d5d3aed8c test: simplify mapped test update 2025-05-06 02:28:05 -07:00
littensy
e276863b2d feat: add detached, add more tests 2025-05-06 02:26:36 -07:00
littensy
7c48bd2fa4 build: install dev deps with wally 2025-05-06 00:33:56 -07:00
littensy
0b37f9cd54 ci: simplify pesde install 2025-05-05 22:38:59 -07:00
littensy
d33c4ce891 fix: update pkgs dir references 2025-05-05 22:18:12 -07:00
littensy
8d385356cd refactor: embrace scope cleanup, restructure code 2025-05-05 22:05:41 -07:00
littensy
3eba56e298 refactor: exclude re-exports from wally 2025-05-05 01:32:51 -07:00
littensy
8df67d6fff refactor: improve imports 2025-05-05 01:30:35 -07:00
littensy
209f9b583b fix: check previous value before calling listener 2025-05-03 21:17:03 -07:00
littensy
a0fc88ede1 fix: effectScope should set scope 2025-05-03 21:16:53 -07:00
littensy
e34b0c9aca refactor: remove pnpm, fix tests 2025-05-03 20:30:10 -07:00
littensy
861ee5acd7 feat: use alien-signals 2025-05-03 18:24:39 -07:00
28 changed files with 90 additions and 533 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
public-hoist-pattern[]=*@rbxts*

149
README.md
View file

@ -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)`

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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 {}

View file

@ -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)

View file

@ -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"

View file

@ -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",

View file

@ -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;
/** /**

View file

@ -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

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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)

View file

@ -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"]

View file

@ -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",

View file

@ -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

View file

@ -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 {}

View file

@ -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"

View file

@ -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",

View file

@ -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 {})

View file

@ -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"

View file

@ -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",

View file

@ -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
View file

@ -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:

View file

@ -1,4 +1,2 @@
packages: packages:
- packages/* - packages/*
publicHoistPattern:
- "*@rbxts*"

View file

@ -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"