mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor!: migrate to monorepo (#23)
* refactor!: create monorepo * chore: prepare release, set up workflows * refactor: rename `modules` to `src` * docs: add docs for all packages * refactor: rename root project file * chore: bump deps * build: rename `lint`, add `pack` script * revert: delete pack script This will not be useful for development.
This commit is contained in:
parent
b038bf1f70
commit
08c7e690d5
81 changed files with 917 additions and 570 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
|
@ -22,15 +22,10 @@ jobs:
|
|||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm wally
|
||||
run: pnpm install
|
||||
|
||||
- name: Check code quality
|
||||
run: pnpm lint
|
||||
run: pnpm check
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm eslint
|
||||
|
||||
- name: Build model
|
||||
run: rojo build -o charm.rbxm
|
||||
- name: Build models
|
||||
run: pnpm build
|
||||
|
|
|
|||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -16,10 +16,17 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: wally install
|
||||
|
||||
- name: Build model
|
||||
run: rojo build -o charm.rbxm
|
||||
- name: Build models
|
||||
run: pnpm build
|
||||
|
||||
- uses: AButler/upload-release-assets@v3.0
|
||||
- name: Upload release assets
|
||||
uses: AButler/upload-release-assets@v3.0
|
||||
with:
|
||||
files: "charm.rbxm"
|
||||
files: "build/*"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm --recursive publish --access public
|
||||
|
||||
- name: Publish to Wally
|
||||
run: pnpm --recursive publish:wally
|
||||
|
|
|
|||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,7 +1,13 @@
|
|||
/node_modules
|
||||
/Packages
|
||||
/DevPackages
|
||||
/build
|
||||
/devpackages
|
||||
/packages
|
||||
/src/**/node_modules
|
||||
/src/**/wally.lock
|
||||
/src/**/README.md
|
||||
/src/**/LICENSE.md
|
||||
/sourcemap.json
|
||||
*.tsbuildinfo
|
||||
*.tgz
|
||||
*.rbxm
|
||||
.DS_Store
|
||||
|
|
|
|||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -1,5 +1,3 @@
|
|||
{
|
||||
"luau-lsp.sourcemap.rojoProjectFile": "dev.project.json",
|
||||
"luau-lsp.types.definitionFiles": ["bin/testez.d.luau"],
|
||||
"luau-lsp.completion.imports.requireStyle": "alwaysRelative"
|
||||
"luau-lsp.types.definitionFiles": ["bin/testez.d.luau"]
|
||||
}
|
||||
|
|
|
|||
173
README.md
173
README.md
|
|
@ -298,14 +298,31 @@ batch(() => {
|
|||
|
||||
---
|
||||
|
||||
## 📘 React
|
||||
## 📦 React
|
||||
|
||||
### Setup
|
||||
|
||||
Install the React bindings for Charm using your package manager of choice.
|
||||
|
||||
```sh
|
||||
npm install @rbxts/react-charm
|
||||
yarn add @rbxts/react-charm
|
||||
pnpm add @rbxts/react-charm
|
||||
```
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ReactCharm = "littensy/react-charm@VERSION"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useAtom(atom, dependencies?)`
|
||||
|
||||
Call `useAtom` at the top-level of a React component to read from an atom.
|
||||
|
||||
```tsx
|
||||
import { useAtom } from "@rbxts/charm";
|
||||
import { useAtom } from "@rbxts/react-charm";
|
||||
import { todosAtom } from "./todos-atom";
|
||||
|
||||
function TodosApp() {
|
||||
|
|
@ -317,7 +334,7 @@ function TodosApp() {
|
|||
By default, the atom is subscribed to once when the component initially mounts. Optionally, you may pass an array of dependencies to `useAtom` if your atom should be memoized based on other values.
|
||||
|
||||
```tsx
|
||||
const todos = useAtom(searchTodos(filter), [filter]);
|
||||
const todos = useAtom(() => searchTodos(filter), [filter]);
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
|
@ -332,27 +349,87 @@ const todos = useAtom(searchTodos(filter), [filter]);
|
|||
|
||||
---
|
||||
|
||||
## 📦 Vide
|
||||
|
||||
### Setup
|
||||
|
||||
Install the Vide bindings for Charm using your package manager of choice.
|
||||
|
||||
```sh
|
||||
npm install @rbxts/vide-charm
|
||||
yarn add @rbxts/vide-charm
|
||||
pnpm add @rbxts/vide-charm
|
||||
```
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
VideCharm = "littensy/vide-charm@VERSION"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useAtom(atom)`
|
||||
|
||||
Call `useAtom` in a any scope to create a Vide source that reads from an atom.
|
||||
|
||||
```tsx
|
||||
import { useAtom } from "@rbxts/vide-charm";
|
||||
import { todosAtom } from "./todos-atom";
|
||||
|
||||
function TodosApp() {
|
||||
const todos = useAtom(todosAtom);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `atom`: An atom or molecule that you want to read from. This can be an atom, or a function that reads from one or more atoms.
|
||||
|
||||
#### Returns
|
||||
|
||||
`useAtom` returns a Vide source.
|
||||
|
||||
---
|
||||
|
||||
## 📗 Charm Sync
|
||||
|
||||
### `sync.server(options)`
|
||||
### Setup
|
||||
|
||||
Call `sync.server` to create a server sync object. The object handles sending state updates to clients at a specified interval, and hydrating clients with the initial state.
|
||||
The Charm Sync package provides server-client synchronization for your Charm atoms. Install it using your package manager of choice.
|
||||
|
||||
```sh
|
||||
npm install @rbxts/charm-sync
|
||||
yarn add @rbxts/charm-sync
|
||||
pnpm add @rbxts/charm-sync
|
||||
```
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
CharmSync = "littensy/charm-sync@VERSION"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `server(options)`
|
||||
|
||||
Call `server` to create a server sync object. The syncer handles sending minimal state patches to clients at a specified interval, and hydrating clients with the initial state.
|
||||
|
||||
```ts
|
||||
import { sync } from "@rbxts/charm";
|
||||
import { server } from "@rbxts/charm-sync";
|
||||
|
||||
const server = sync.server({
|
||||
const syncer = server({
|
||||
atoms: atomsToSync,
|
||||
interval: 0,
|
||||
preserveHistory: false,
|
||||
});
|
||||
|
||||
server.connect((player, ...payloads) => {
|
||||
syncer.connect((player, ...payloads) => {
|
||||
remotes.syncState.fire(player, ...payloads);
|
||||
});
|
||||
|
||||
remotes.requestState.connect((player) => {
|
||||
server.hydrate(player);
|
||||
syncer.hydrate(player);
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -368,11 +445,11 @@ remotes.requestState.connect((player) => {
|
|||
|
||||
#### Returns
|
||||
|
||||
`sync.server` returns a server sync object. The sync object has the following methods:
|
||||
`server` returns a server sync object. The sync object has the following methods:
|
||||
|
||||
- `server.connect(callback)` registers a callback to send state updates to clients. The callback will receive the player and the payload to send, and should send the payload to the client. The payload should not be mutated, so changes should be applied to a copy of the payload.
|
||||
- `syncer.connect(callback)` registers a callback to send state updates to clients. The callback will receive the player and the payload to send, and should send the payload to the client. The payload should not be mutated, so changes should be applied to a copy of the payload.
|
||||
|
||||
- `server.hydrate(player)` sends the initial state to a player. This calls the function passed to `connect` with a payload containing the initial state.
|
||||
- `syncer.hydrate(player)` sends the initial state to a player. This calls the function passed to `connect` with a payload containing the initial state.
|
||||
|
||||
#### Caveats
|
||||
|
||||
|
|
@ -382,17 +459,17 @@ remotes.requestState.connect((player) => {
|
|||
|
||||
---
|
||||
|
||||
### `sync.client(options)`
|
||||
### `client(options)`
|
||||
|
||||
Call `sync.client` to create a client sync object. The object will sync the client's copy of the state with the server's state.
|
||||
Call `client` to create a client sync object. The syncer will hydrate the client's copy of the state with the server's state.
|
||||
|
||||
```ts
|
||||
import { sync } from "@rbxts/charm";
|
||||
import { client } from "@rbxts/charm-sync";
|
||||
|
||||
const client = sync.client({ atoms: atomsToSync });
|
||||
const syncer = client({ atoms: atomsToSync });
|
||||
|
||||
remotes.syncState.connect((...payloads) => {
|
||||
client.sync(...payloads);
|
||||
syncer.sync(...payloads);
|
||||
});
|
||||
|
||||
remotes.requestState.fire();
|
||||
|
|
@ -406,9 +483,9 @@ remotes.requestState.fire();
|
|||
|
||||
#### Returns
|
||||
|
||||
`sync.client` returns a client sync object. The sync object has the following methods:
|
||||
`client` returns a client sync object. The sync object has the following methods:
|
||||
|
||||
- `client.sync(...payloads)` applies a state update from the server.
|
||||
- `syncer.sync(...payloads)` applies a state update from the server.
|
||||
|
||||
#### Caveats
|
||||
|
||||
|
|
@ -416,18 +493,18 @@ remotes.requestState.fire();
|
|||
|
||||
---
|
||||
|
||||
### `sync.isNone(value)`
|
||||
|
||||
State patches represent the _difference_ between the current state and next state, excluding unchanged values. However, this means both unchanged and removed values would be `nil` in the patch. In these cases, Charm uses the `None` marker to represent a removed value.
|
||||
### `isNone(value)`
|
||||
|
||||
Call `sync.isNone` to check if a value is `None`.
|
||||
|
||||
State patches represent the _difference_ between the current state and next state, excluding unchanged values. However, this means both unchanged and removed values would be `nil` in the patch. In these cases, Charm uses the `None` marker to represent a removed value.
|
||||
|
||||
```ts
|
||||
import { sync } from "@rbxts/charm";
|
||||
import { isNone, server } from "@rbxts/charm-sync";
|
||||
|
||||
const server = sync.server({ atoms: atomsToSync });
|
||||
const syncer = server({ atoms: atomsToSync });
|
||||
|
||||
server.connect((player, payload) => {
|
||||
syncer.connect((player, payload) => {
|
||||
if (payload.type === "patch" && sync.isNone(payload.data.todosAtom?.eggs)) {
|
||||
// 'eggs' will be removed from the client's todos atom
|
||||
}
|
||||
|
|
@ -437,11 +514,11 @@ server.connect((player, payload) => {
|
|||
|
||||
#### Parameters
|
||||
|
||||
- `value`: Any value. If the value is `None`, `sync.isNone` will return `true`.
|
||||
- `value`: Any value. If the value is `None`, `isNone` will return `true`.
|
||||
|
||||
#### Returns
|
||||
|
||||
`sync.isNone` returns a boolean.
|
||||
`isNone` returns a boolean.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -466,18 +543,18 @@ counterAtom(1); // 2
|
|||
counterAtom((count) => count + 1); // 4
|
||||
```
|
||||
|
||||
### Counter component
|
||||
### React component
|
||||
|
||||
```tsx
|
||||
import React from "@rbxts/react";
|
||||
import { useAtom } from "@rbxts/charm";
|
||||
import { useAtom } from "@rbxts/react-charm";
|
||||
import { counterAtom, incrementCounter } from "./counter-atom";
|
||||
|
||||
function Counter() {
|
||||
const count = useAtom(counterAtom);
|
||||
|
||||
return (
|
||||
<textlabel
|
||||
<textbutton
|
||||
Text={`Count: ${count}`}
|
||||
Size={new UDim2(0, 100, 0, 50)}
|
||||
Event={{
|
||||
|
|
@ -488,6 +565,28 @@ function Counter() {
|
|||
}
|
||||
```
|
||||
|
||||
### Vide component
|
||||
|
||||
```tsx
|
||||
import Vide from "@rbxts/vide";
|
||||
import { useAtom } from "@rbxts/vide-charm";
|
||||
import { counterAtom, incrementCounter } from "./counter-atom";
|
||||
|
||||
function Counter() {
|
||||
const count = useAtom(counterAtom);
|
||||
|
||||
return (
|
||||
<textbutton
|
||||
Activated={() => {
|
||||
incrementCounter();
|
||||
}}
|
||||
Text={() => `Count: ${count()}`}
|
||||
Size={new UDim2(0, 100, 0, 50)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server-client sync
|
||||
|
||||
Charm provides client and server objects for synchronizing state between the server and clients. Start by defining a module (or creating an object) exporting the atoms you want to sync:
|
||||
|
|
@ -502,35 +601,35 @@ Then, on the server, create a server sync object and pass in the atoms to sync.
|
|||
Note that if `preserveHistory` is `true`, the server will send multiple payloads to the client, so the callback passed to `connect` should accept a `...payloads` parameter. Otherwise, you only need to handle a single `payload` parameter.
|
||||
|
||||
```ts
|
||||
import { sync } from "@rbxts/charm";
|
||||
import { server } from "@rbxts/charm-sync";
|
||||
import { remotes } from "./remotes";
|
||||
import * as atoms from "./atoms";
|
||||
|
||||
const server = sync.server({ atoms });
|
||||
const syncer = server({ atoms });
|
||||
|
||||
// Broadcast a state update to a specific player
|
||||
server.connect((player, ...payloads) => {
|
||||
syncer.connect((player, ...payloads) => {
|
||||
remotes.syncState.fire(player, ...payloads);
|
||||
});
|
||||
|
||||
// Send initial state to a player upon request
|
||||
remotes.requestState.connect((player) => {
|
||||
server.hydrate(player);
|
||||
syncer.hydrate(player);
|
||||
});
|
||||
```
|
||||
|
||||
Finally, on the client, create a client sync object and apply incoming state changes.
|
||||
|
||||
```ts
|
||||
import { sync } from "@rbxts/charm";
|
||||
import { client } from "@rbxts/charm-sync";
|
||||
import { remotes } from "./remotes";
|
||||
import * as atoms from "./atoms";
|
||||
|
||||
const client = sync.client({ atoms });
|
||||
const syncer = sync.client({ atoms });
|
||||
|
||||
// Listen for incoming state changes from the server
|
||||
remotes.syncState.connect((...payloads) => {
|
||||
client.sync(...payloads);
|
||||
syncer.sync(...payloads);
|
||||
});
|
||||
|
||||
// Request initial state from the server
|
||||
|
|
|
|||
5
bin/build.sh
Normal file
5
bin/build.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mkdir build
|
||||
rojo build src/charm -o build/charm.rbxm
|
||||
rojo build src/charm-sync -o build/charm-sync.rbxm
|
||||
rojo build src/react -o build/react-charm.rbxm
|
||||
rojo build src/vide -o build/vide-charm.rbxm
|
||||
29
bin/check.sh
29
bin/check.sh
|
|
@ -1,18 +1,25 @@
|
|||
curl -o bin/roblox.d.luau https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.lua
|
||||
|
||||
rojo sourcemap dev.project.json -o sourcemap.json
|
||||
rojo sourcemap -o sourcemap.json
|
||||
|
||||
luau-lsp analyze \
|
||||
--defs=bin/roblox.d.luau \
|
||||
--defs=bin/testez.d.luau \
|
||||
--flag:LuauFixIndexerSubtypingOrdering=true \
|
||||
--flag:LuauInstantiateInSubtyping=true \
|
||||
--flag:LuauTinyControlFlowAnalysis=true \
|
||||
--sourcemap=sourcemap.json \
|
||||
--ignore="**/_Index/**" src
|
||||
check() {
|
||||
echo "Checking $1"
|
||||
|
||||
luau-lsp analyze \
|
||||
--defs=bin/roblox.d.luau \
|
||||
--defs=bin/testez.d.luau \
|
||||
--flag:LuauFixIndexerSubtypingOrdering=true \
|
||||
--flag:LuauInstantiateInSubtyping=true \
|
||||
--sourcemap=sourcemap.json \
|
||||
--ignore="**/_Index/**" \
|
||||
$1
|
||||
|
||||
selene $1
|
||||
stylua --check $1
|
||||
}
|
||||
|
||||
selene src
|
||||
stylua --check src
|
||||
eslint src
|
||||
check src/*/src
|
||||
check tests
|
||||
|
||||
rm bin/roblox.d.luau
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
wally install
|
||||
rojo sourcemap dev.project.json --output sourcemap.json
|
||||
wally-package-types --sourcemap sourcemap.json Packages
|
||||
wally-package-types --sourcemap sourcemap.json DevPackages
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
--!nocheck
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Packages = ReplicatedStorage.Packages
|
||||
local DevPackages = ReplicatedStorage.DevPackages
|
||||
|
||||
local TestEZ = require(DevPackages.TestEZ)
|
||||
|
||||
TestEZ.TestBootstrap:run({ Packages.charm.__tests__ }, TestEZ.Reporters.TextReporter)
|
||||
|
|
@ -1,6 +1,30 @@
|
|||
{
|
||||
"name": "charm",
|
||||
"name": "charm-test",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"DevPackages": {
|
||||
"$path": "DevPackages",
|
||||
"Charm": {
|
||||
"$path": "src/charm"
|
||||
},
|
||||
"CharmSync": {
|
||||
"$path": "src/charm-sync"
|
||||
},
|
||||
"ReactCharm": {
|
||||
"$path": "src/react"
|
||||
},
|
||||
"VideCharm": {
|
||||
"$path": "src/vide"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
"Tests": {
|
||||
"$path": "Tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "charm-dev",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"DevPackages": {
|
||||
"$path": "DevPackages",
|
||||
"Charm": { "$path": "src" }
|
||||
},
|
||||
"Packages": { "$path": "Packages" }
|
||||
},
|
||||
"TestService": {
|
||||
"$className": "TestService",
|
||||
"$properties": { "ExecuteWithStudioRun": true },
|
||||
"spec": { "$path": "bin/spec.server.luau" }
|
||||
}
|
||||
}
|
||||
}
|
||||
49
package.json
49
package.json
|
|
@ -1,40 +1,13 @@
|
|||
{
|
||||
"name": "@rbxts/charm",
|
||||
"version": "0.7.6",
|
||||
"description": "Atomic state management for Roblox",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "sh bin/check.sh",
|
||||
"wally": "sh bin/install.sh"
|
||||
},
|
||||
"main": "src/init.lua",
|
||||
"types": "src/index.d.ts",
|
||||
"keywords": [
|
||||
"roblox",
|
||||
"typescript",
|
||||
"roblox-ts",
|
||||
"state"
|
||||
],
|
||||
"author": "littensy",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/littensy/charm"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/littensy/charm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/littensy/charm",
|
||||
"files": [
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"build": "sh bin/build.sh",
|
||||
"check": "sh bin/check.sh",
|
||||
"wally": "wally install && rojo sourcemap --output sourcemap.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rbxts/compiler-types": "3.0.0-types.0",
|
||||
"@rbxts/react": "^0.4.0",
|
||||
"@rbxts/types": "^1.0.809",
|
||||
"@rbxts/types": "^1.0.811",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"eslint": "^8.57.1",
|
||||
|
|
@ -42,15 +15,7 @@
|
|||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-roblox-ts": "^0.0.36",
|
||||
"prettier": "^3.3.3",
|
||||
"roblox-ts": "3.0.0",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/react": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rbxts/react": {
|
||||
"optional": true
|
||||
}
|
||||
"roblox-ts": "^3.0.0",
|
||||
"typescript": "~5.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
181
pnpm-lock.yaml
generated
181
pnpm-lock.yaml
generated
|
|
@ -11,18 +11,15 @@ importers:
|
|||
'@rbxts/compiler-types':
|
||||
specifier: 3.0.0-types.0
|
||||
version: 3.0.0-types.0
|
||||
'@rbxts/react':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
'@rbxts/types':
|
||||
specifier: ^1.0.809
|
||||
version: 1.0.809
|
||||
specifier: ^1.0.811
|
||||
version: 1.0.811
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^7.18.0
|
||||
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)
|
||||
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.18.0
|
||||
version: 7.18.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
version: 7.18.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
|
|
@ -39,11 +36,63 @@ importers:
|
|||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
roblox-ts:
|
||||
specifier: 3.0.0
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
typescript:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
specifier: ~5.5.4
|
||||
version: 5.5.4
|
||||
|
||||
modules/charm: {}
|
||||
|
||||
modules/charm-sync:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
|
||||
modules/react:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
'@rbxts/react':
|
||||
specifier: '*'
|
||||
version: 17.2.1
|
||||
|
||||
modules/vide:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
'@rbxts/vide':
|
||||
specifier: '*'
|
||||
version: 0.4.6
|
||||
|
||||
src/charm: {}
|
||||
|
||||
src/charm-sync:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
|
||||
src/react:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
'@rbxts/react':
|
||||
specifier: '*'
|
||||
version: 17.2.1
|
||||
|
||||
src/vide:
|
||||
dependencies:
|
||||
'@rbxts/charm':
|
||||
specifier: workspace:*
|
||||
version: link:../charm
|
||||
'@rbxts/vide':
|
||||
specifier: '*'
|
||||
version: 0.4.6
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -97,14 +146,17 @@ packages:
|
|||
'@rbxts/compiler-types@3.0.0-types.0':
|
||||
resolution: {integrity: sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==}
|
||||
|
||||
'@rbxts/react-vendor@0.4.0':
|
||||
resolution: {integrity: sha512-roWs9FmR7CQDK1N6gE/36tiCBQRl8v1S0pUJD2Ly3ZMVvfjAk2gixrkqGcKFGtXQQq6r4dTAtHduAeZJBd640g==}
|
||||
'@rbxts/react-vendor@17.2.1':
|
||||
resolution: {integrity: sha512-7DxFweCbhhso4M4tl1pO7Kil9SZrvOl1SoZ4lnwUfoxdHvSmWgeKgaUqd+ZS3/u1E8kRraqGQ56Q8zjUEEUzlA==}
|
||||
|
||||
'@rbxts/react@0.4.0':
|
||||
resolution: {integrity: sha512-yhqXzfH5YpwfZyiWipQMfQsI//2sC86Jqn2rMkP+l1y4CfhCvoYxpaI5qS7Xum9F+DsBHiwJNeUxm2V2LzXW0A==}
|
||||
'@rbxts/react@17.2.1':
|
||||
resolution: {integrity: sha512-iu0RsVvfHroI7l+QXjwE0FiWV2rXoKMxuznDZ8vnx2BsFqrPnCqFjXuNeGYZwibibZC6jbyq7uKl+/8cz7XlsA==}
|
||||
|
||||
'@rbxts/types@1.0.809':
|
||||
resolution: {integrity: sha512-COmNdE5VKq44tU1Sqbj0b70qDcUpBY1Uu5BhNb25v1yJATwcoVjsmYTyaxmRO4EVdqOATjOUBU2jbxZa4K83uw==}
|
||||
'@rbxts/types@1.0.811':
|
||||
resolution: {integrity: sha512-MPhs5lxzxvDqie34Y9FzJ1R+k+33jjrkj03agX2y2WzBKyGmO0rD/kPuUaKXQgEiACXuk36C5FxUTYgkKJPbvA==}
|
||||
|
||||
'@rbxts/vide@0.4.6':
|
||||
resolution: {integrity: sha512-xYtvCS05nGq1YU7fyQVmp4z1CcvuOR+VKpKj/Y7lFPvKobQ3oHTQEzZiJFEMZ+iSeHxwZhwpzVjrP+6+e2EoQw==}
|
||||
|
||||
'@roblox-ts/luau-ast@2.0.0':
|
||||
resolution: {integrity: sha512-cmMi093IdwBOLVxwuordhM8AmtbyTIyRpsTbB0D/JauidW4SXsQRQowSwWjHo4QP0DRJBXvOIlxtqEQi50uNzQ==}
|
||||
|
|
@ -118,8 +170,8 @@ packages:
|
|||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@20.16.9':
|
||||
resolution: {integrity: sha512-rkvIVJxsOfBejxK7I0FO5sa2WxFmJCzoDwcd88+fq/CUfynNywTo/1/T6hyFz22CyztsnLS9nVlHOnTI36RH5w==}
|
||||
'@types/node@20.16.11':
|
||||
resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==}
|
||||
|
||||
'@types/semver@7.5.8':
|
||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||
|
|
@ -370,6 +422,7 @@ packages:
|
|||
eslint@8.57.1:
|
||||
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
|
||||
espree@9.6.1:
|
||||
|
|
@ -737,8 +790,8 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
synckit@0.9.1:
|
||||
resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==}
|
||||
synckit@0.9.2:
|
||||
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
text-table@0.2.0:
|
||||
|
|
@ -779,8 +832,8 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.6.2:
|
||||
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
|
||||
typescript@5.5.4:
|
||||
resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -879,13 +932,15 @@ snapshots:
|
|||
|
||||
'@rbxts/compiler-types@3.0.0-types.0': {}
|
||||
|
||||
'@rbxts/react-vendor@0.4.0': {}
|
||||
'@rbxts/react-vendor@17.2.1': {}
|
||||
|
||||
'@rbxts/react@0.4.0':
|
||||
'@rbxts/react@17.2.1':
|
||||
dependencies:
|
||||
'@rbxts/react-vendor': 0.4.0
|
||||
'@rbxts/react-vendor': 17.2.1
|
||||
|
||||
'@rbxts/types@1.0.809': {}
|
||||
'@rbxts/types@1.0.811': {}
|
||||
|
||||
'@rbxts/vide@0.4.6': {}
|
||||
|
||||
'@roblox-ts/luau-ast@2.0.0': {}
|
||||
|
||||
|
|
@ -901,48 +956,48 @@ snapshots:
|
|||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@20.16.9':
|
||||
'@types/node@20.16.11':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/semver@7.5.8': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.11.1
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
eslint: 8.57.1
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/experimental-utils@5.62.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/experimental-utils@5.62.0(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
eslint: 8.57.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4)
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
debug: 4.3.7
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -956,15 +1011,15 @@ snapshots:
|
|||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
|
||||
'@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
debug: 4.3.7
|
||||
eslint: 8.57.1
|
||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -972,7 +1027,7 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/types@7.18.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.2)':
|
||||
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
'@typescript-eslint/visitor-keys': 5.62.0
|
||||
|
|
@ -980,13 +1035,13 @@ snapshots:
|
|||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.6.3
|
||||
tsutils: 3.21.0(typescript@5.6.2)
|
||||
tsutils: 3.21.0(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.2)':
|
||||
'@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
|
|
@ -995,20 +1050,20 @@ snapshots:
|
|||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.6.3
|
||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.8
|
||||
'@typescript-eslint/scope-manager': 5.62.0
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.2)
|
||||
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.5.4)
|
||||
eslint: 8.57.1
|
||||
eslint-scope: 5.1.1
|
||||
semver: 7.6.3
|
||||
|
|
@ -1016,12 +1071,12 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
'@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4)
|
||||
eslint: 8.57.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -1159,15 +1214,15 @@ snapshots:
|
|||
eslint: 8.57.1
|
||||
prettier: 3.3.3
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.9.1
|
||||
synckit: 0.9.2
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 9.1.0(eslint@8.57.1)
|
||||
|
||||
eslint-plugin-roblox-ts@0.0.36(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@types/node': 20.16.9
|
||||
'@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
typescript: 5.6.2
|
||||
'@types/node': 20.16.11
|
||||
'@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
typescript: 5.5.4
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
- supports-color
|
||||
|
|
@ -1554,7 +1609,7 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
synckit@0.9.1:
|
||||
synckit@0.9.2:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.1.1
|
||||
tslib: 2.7.0
|
||||
|
|
@ -1565,18 +1620,18 @@ snapshots:
|
|||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
ts-api-utils@1.3.0(typescript@5.6.2):
|
||||
ts-api-utils@1.3.0(typescript@5.5.4):
|
||||
dependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.7.0: {}
|
||||
|
||||
tsutils@3.21.0(typescript@5.6.2):
|
||||
tsutils@3.21.0(typescript@5.5.4):
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
typescript: 5.6.2
|
||||
typescript: 5.5.4
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
|
|
@ -1586,7 +1641,7 @@ snapshots:
|
|||
|
||||
typescript@5.5.3: {}
|
||||
|
||||
typescript@5.6.2: {}
|
||||
typescript@5.5.4: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
|
|
|
|||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
packages:
|
||||
- src/*
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
return function()
|
||||
local previous = _G.__DEV__
|
||||
|
||||
beforeAll(function()
|
||||
_G.__DEV__ = false -- prevent React errors
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
_G.__DEV__ = previous
|
||||
end)
|
||||
end
|
||||
6
src/charm-sync/default.project.json
Normal file
6
src/charm-sync/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "charm-sync",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
16
src/charm-sync/package.json
Normal file
16
src/charm-sync/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@rbxts/charm-sync",
|
||||
"version": "0.1.0",
|
||||
"description": "Sync server state with clients using Charm",
|
||||
"repository": "littensy/charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
local store = require(script.Parent.Parent.store)
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type SyncPayload = types.SyncPayload
|
||||
local Charm = require(script.Parent.Parent.Charm)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
type ClientOptions = {
|
||||
--[=[
|
||||
The atoms to synchronize with the server.
|
||||
]=]
|
||||
atoms: { [string]: Atom<any> },
|
||||
atoms: { [string]: Charm.Atom<any> },
|
||||
--[=[
|
||||
Whether to ignore patches sent before the client has been hydrated.
|
||||
Default is `true`.
|
||||
|
|
@ -52,7 +51,7 @@ local function client(options: ClientOptions): ClientSyncer
|
|||
|
||||
hydrated = true
|
||||
|
||||
store.batch(function()
|
||||
Charm.batch(function()
|
||||
for key, state in next, payload.data do
|
||||
local atom = atoms[key]
|
||||
|
||||
163
src/charm-sync/src/index.d.ts
vendored
Normal file
163
src/charm-sync/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { Atom, Molecule } from "@rbxts/charm";
|
||||
|
||||
export = CharmSync;
|
||||
export as namespace CharmSync;
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
||||
declare namespace CharmSync {
|
||||
type AtomMap = Record<string, Atom<any>>;
|
||||
|
||||
type MoleculeMap = Record<string, Molecule<any>>;
|
||||
|
||||
/**
|
||||
* Recursively infers the type of the state produced by a map of molecules.
|
||||
*/
|
||||
type StateOfMap<T> = {
|
||||
[P in keyof T]: T[P] extends Molecule<infer State> ? State : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* A special value that denotes the absence of a value. Used to represent
|
||||
* removed values in patches.
|
||||
*/
|
||||
interface None {
|
||||
readonly __none: "__none";
|
||||
}
|
||||
|
||||
type MaybeNone<T> = undefined extends T ? None : never;
|
||||
|
||||
/**
|
||||
* Creates a `ClientSyncer` object that receives patches from the server and
|
||||
* applies them to the local state.
|
||||
*
|
||||
* @client
|
||||
* @param options The atoms to synchronize with the server.
|
||||
* @returns A `ClientSyncer` object.
|
||||
*/
|
||||
function client<Atoms extends AtomMap>(options: ClientOptions<Atoms>): ClientSyncer<Atoms>;
|
||||
|
||||
/**
|
||||
* Creates a `ServerSyncer` object that sends patches to the client and
|
||||
* hydrates the client's state.
|
||||
*
|
||||
* @server
|
||||
* @param options The atoms to synchronize with the client.
|
||||
* @returns A `ServerSyncer` object.
|
||||
*/
|
||||
function server<Molecules extends MoleculeMap>(options: ServerOptions<Molecules>): ServerSyncer<Molecules>;
|
||||
|
||||
/**
|
||||
* Checks whether a value is `None`. If `true`, the value is scheduled to be
|
||||
* removed from the state when the patch is applied.
|
||||
*
|
||||
* @param value The value to check.
|
||||
* @returns `true` if the value is `None`, otherwise `false`.
|
||||
*/
|
||||
function isNone(value: unknown): value is None;
|
||||
|
||||
type DataTypes = {
|
||||
[P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* A type that should not be made partial in patches.
|
||||
*/
|
||||
type DataType = DataTypes[keyof DataTypes];
|
||||
|
||||
/**
|
||||
* A partial patch that can be applied to the state to update it. Represents
|
||||
* the difference between the current state and the next state.
|
||||
*
|
||||
* If a value was removed, it is replaced with `None`. This can be checked
|
||||
* using the `sync.isNone` function.
|
||||
*/
|
||||
type SyncPatch<State> =
|
||||
| MaybeNone<State>
|
||||
| (State extends ReadonlyMap<infer K, infer V> | Map<infer K, infer V>
|
||||
? ReadonlyMap<K, SyncPatch<V> | None>
|
||||
: State extends Set<infer T> | ReadonlySet<infer T>
|
||||
? ReadonlyMap<T, true | None>
|
||||
: State extends readonly (infer T)[]
|
||||
? readonly (SyncPatch<T> | None | undefined)[]
|
||||
: State extends DataType
|
||||
? State
|
||||
: State extends object
|
||||
? { readonly [P in keyof State]?: SyncPatch<State[P]> }
|
||||
: State);
|
||||
|
||||
/**
|
||||
* A payload that can be sent from the server to the client to synchronize
|
||||
* state between the two.
|
||||
*/
|
||||
type SyncPayload<Molecules extends MoleculeMap> =
|
||||
| { type: "init"; data: StateOfMap<Molecules> }
|
||||
| { type: "patch"; data: SyncPatch<StateOfMap<Molecules>> };
|
||||
|
||||
interface ClientOptions<Atoms extends AtomMap> {
|
||||
/**
|
||||
* The atoms to synchronize with the server.
|
||||
*/
|
||||
atoms: Atoms;
|
||||
/**
|
||||
* Whether to ignore patches sent before the client has been hydrated.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
ignoreUnhydrated?: boolean;
|
||||
}
|
||||
|
||||
interface ServerOptions<Molecules extends MoleculeMap> {
|
||||
/**
|
||||
* The atoms to synchronize with the client.
|
||||
*/
|
||||
atoms: Molecules;
|
||||
/**
|
||||
* The interval at which to send patches to the client, in seconds.
|
||||
* Defaults to `0` (patches are sent up to once per frame). Set to a
|
||||
* negative value to disable automatic syncing.
|
||||
*/
|
||||
interval?: number;
|
||||
/**
|
||||
* Whether the history of state changes since the client's last update
|
||||
* should be preserved. This is useful for values that change multiple times
|
||||
* per frame, where each individual change is important. Defaults to `false`.
|
||||
*
|
||||
* If `true`, the broadcaster will send a list of payloads to the client
|
||||
* instead of a single payload. The client will apply each payload in order
|
||||
* to reconstruct the state's changes over time.
|
||||
*/
|
||||
preserveHistory?: boolean;
|
||||
}
|
||||
|
||||
interface ClientSyncer<Atoms extends AtomMap> {
|
||||
/**
|
||||
* Applies a patch or initializes the state of the atoms with the given
|
||||
* payload from the server.
|
||||
*
|
||||
* @param payloads The patches or hydration payloads to apply.
|
||||
*/
|
||||
sync(...payloads: SyncPayload<Atoms>[]): void;
|
||||
}
|
||||
|
||||
interface ServerSyncer<Molecules extends MoleculeMap> {
|
||||
/**
|
||||
* Sets up a subscription to each atom that schedules a patch to be sent to
|
||||
* the client whenever the state changes. When a change occurs, the `callback`
|
||||
* is called with the player and the payloads to send.
|
||||
*
|
||||
* Note that a `payload` object should not be mutated. If you need to modify
|
||||
* a payload, apply the changes to a copy of the object.
|
||||
*
|
||||
* @param callback The function to call when the state changes.
|
||||
* @returns A cleanup function that unsubscribes all listeners.
|
||||
*/
|
||||
connect(callback: (player: Player, ...payloads: SyncPayload<Molecules>[]) => void): Cleanup;
|
||||
/**
|
||||
* Hydrates the client's state with the server's state. This should be
|
||||
* called when a player joins the game and requires the server's state.
|
||||
*
|
||||
* @param player The player to hydrate.
|
||||
*/
|
||||
hydrate(player: Player): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
local client = require(script.client)
|
||||
local patch = require(script.patch)
|
||||
local server = require(script.server)
|
||||
local types = require(script.types)
|
||||
|
||||
export type SyncPayload = types.SyncPayload
|
||||
|
||||
--[=[
|
||||
Synchronizes state between the client and server. The server sends patches
|
||||
|
|
@ -7,7 +7,7 @@ local RunService = game:GetService("RunService")
|
|||
@param seconds The interval in seconds.
|
||||
@return A function that cancels the interval.
|
||||
]=]
|
||||
local function setInterval(callback: () -> (), seconds: number): () -> ()
|
||||
local function interval(callback: () -> (), seconds: number): () -> ()
|
||||
if seconds < 0 then
|
||||
return function() end
|
||||
end
|
||||
|
|
@ -27,4 +27,4 @@ local function setInterval(callback: () -> (), seconds: number): () -> ()
|
|||
end
|
||||
end
|
||||
|
||||
return setInterval
|
||||
return interval
|
||||
|
|
@ -4,7 +4,7 @@ local validate = require(script.Parent.validate)
|
|||
A special symbol that denotes the absence of a value. Used to represent
|
||||
deleted values in patches.
|
||||
]=]
|
||||
local NONE = { __none = "__none" }
|
||||
local None = { __none = "__none" }
|
||||
|
||||
local function isNone(value: any): boolean
|
||||
return type(value) == "table" and value.__none == "__none"
|
||||
|
|
@ -46,7 +46,7 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
|||
if previous == current then
|
||||
patches[key] = nil
|
||||
elseif current == nil then
|
||||
patches[key] = NONE
|
||||
patches[key] = None
|
||||
elseif type(previous) == "table" and type(current) == "table" then
|
||||
patches[key] = diff(previous, current)
|
||||
end
|
||||
|
|
@ -97,7 +97,7 @@ local function apply(state: any, patches: any): any
|
|||
end
|
||||
|
||||
return {
|
||||
NONE = NONE,
|
||||
NONE = None,
|
||||
isNone = isNone,
|
||||
diff = diff,
|
||||
apply = apply,
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type SyncPayload = types.SyncPayload
|
||||
local setInterval = require(script.Parent.Parent.utils.setInterval)
|
||||
local Charm = require(script.Parent.Parent.Charm)
|
||||
local interval = require(script.Parent.interval)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
type ServerOptions = {
|
||||
--[=[
|
||||
The atoms to synchronize with the client.
|
||||
]=]
|
||||
atoms: { [string]: Atom<any> },
|
||||
atoms: { [string]: Charm.Atom<any> },
|
||||
--[=[
|
||||
The interval at which to send patches to the client, in seconds.
|
||||
Defaults to `0` (patches are sent up to once per frame). Set to a
|
||||
|
|
@ -103,7 +102,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
-- Populate the snapshot with the initial state of each atom.
|
||||
-- Subscribe to each atom and update the state when it changes.
|
||||
for key, atom in next, options.atoms do
|
||||
subscriptions[key] = subscribe(atom, function(current, previous)
|
||||
subscriptions[key] = Charm.subscribe(atom, function(current, previous)
|
||||
pushSnapshot(key, current, previous)
|
||||
changed = true
|
||||
end)
|
||||
|
|
@ -111,7 +110,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
|
||||
snapshots[1] = createSnapshot()
|
||||
|
||||
local disconnect = setInterval(function()
|
||||
local disconnect = interval(function()
|
||||
if not changed then
|
||||
return
|
||||
end
|
||||
|
|
@ -187,13 +186,13 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
|
||||
-- Subscribe to each atom and update the state when one changes.
|
||||
for key, atom in next, options.atoms do
|
||||
subscriptions[key] = subscribe(atom, function(state)
|
||||
subscriptions[key] = Charm.subscribe(atom, function(state)
|
||||
currentSnapshot[key] = state
|
||||
changed = true
|
||||
end)
|
||||
end
|
||||
|
||||
local disconnect = setInterval(function()
|
||||
local disconnect = interval(function()
|
||||
if not changed then
|
||||
return
|
||||
end
|
||||
10
src/charm-sync/src/types.luau
Normal file
10
src/charm-sync/src/types.luau
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
--[=[
|
||||
A payload that can be sent from the server to the client to synchronize
|
||||
state between the two.
|
||||
]=]
|
||||
export type SyncPayload = {
|
||||
type: "patch" | "init",
|
||||
data: { [any]: any },
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
local store = require(script.Parent.Parent.store)
|
||||
local Charm = require(script.Parent.Parent.Charm)
|
||||
|
||||
local SAFE_KEYS = { string = true, number = true }
|
||||
local SAFE_VALUES = { string = true, number = true, boolean = true, table = true, userdata = true }
|
||||
|
|
@ -47,7 +47,7 @@ local function validate(value: any, key: any)
|
|||
elseif typeOfValue == "table" then
|
||||
if getmetatable(value) ~= nil then
|
||||
error(`Cannot sync tables with metatables! Got {value} at key '{key}'`)
|
||||
elseif store.isAtom(value) then
|
||||
elseif Charm.isAtom(value) then
|
||||
error(`Cannot sync nested atoms! Got atom at key '{key}'`)
|
||||
elseif isUnsafeTable(value) then
|
||||
error(
|
||||
7
src/charm-sync/wally.toml
Normal file
7
src/charm-sync/wally.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "littensy/charm-sync"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
3
src/charm/.luaurc
Normal file
3
src/charm/.luaurc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"languageMode": "strict"
|
||||
}
|
||||
6
src/charm/default.project.json
Normal file
6
src/charm/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
13
src/charm/package.json
Normal file
13
src/charm/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@rbxts/charm",
|
||||
"version": "0.8.0",
|
||||
"description": "An atomic state management library for Roblox",
|
||||
"repository": "littensy/charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
}
|
||||
}
|
||||
172
src/index.d.ts → src/charm/src/index.d.ts
vendored
172
src/index.d.ts → src/charm/src/index.d.ts
vendored
|
|
@ -42,16 +42,6 @@ declare namespace Charm {
|
|||
*/
|
||||
type StateOf<T> = T extends Molecule<infer State> ? State : never;
|
||||
|
||||
/**
|
||||
* Recursively infers the type of the state produced by a map of molecules.
|
||||
*/
|
||||
type StateOfMap<T> = {
|
||||
[P in keyof T]: T[P] extends Molecule<infer State> ? State : never;
|
||||
};
|
||||
|
||||
type AtomMap = Record<string, Atom<any>>;
|
||||
type MoleculeMap = Record<string, Molecule<any>>;
|
||||
|
||||
interface AtomOptions<State> {
|
||||
/**
|
||||
* A function that determines whether the state has changed. By default,
|
||||
|
|
@ -189,166 +179,4 @@ declare namespace Charm {
|
|||
molecule: Molecule<AnyMap<K0, V0>>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
||||
): Molecule<ReadonlyMap<K1, V1>>;
|
||||
|
||||
/**
|
||||
* A hook that subscribes to changes in the given atom or molecule. The
|
||||
* component is re-rendered whenever the state changes.
|
||||
*
|
||||
* If the `dependencies` array is provided, the subscription to the atom or
|
||||
* molecule is re-created whenever the dependencies change. Otherwise, the
|
||||
* subscription is created once when the component is mounted.
|
||||
*
|
||||
* @param molecule The atom or molecule to subscribe to.
|
||||
* @param dependencies An array of values that the subscription depends on.
|
||||
* @returns The current state.
|
||||
*/
|
||||
function useAtom<State>(molecule: Molecule<State>, dependencies?: unknown[]): State;
|
||||
|
||||
/**
|
||||
* Synchronizes state between the client and server. The server sends patches
|
||||
* to the client, which applies them to its local state.
|
||||
*/
|
||||
const sync: {
|
||||
/**
|
||||
* Creates a `ClientSyncer` object that receives patches from the server and
|
||||
* applies them to the local state.
|
||||
*
|
||||
* @client
|
||||
* @param options The atoms to synchronize with the server.
|
||||
* @returns A `ClientSyncer` object.
|
||||
*/
|
||||
client: <Atoms extends AtomMap>(options: ClientOptions<Atoms>) => ClientSyncer<Atoms>;
|
||||
/**
|
||||
* Creates a `ServerSyncer` object that sends patches to the client and
|
||||
* hydrates the client's state.
|
||||
*
|
||||
* @server
|
||||
* @param options The atoms to synchronize with the client.
|
||||
* @returns A `ServerSyncer` object.
|
||||
*/
|
||||
server: <Molecules extends MoleculeMap>(options: ServerOptions<Molecules>) => ServerSyncer<Molecules>;
|
||||
/**
|
||||
* Checks whether a value is `None`. If `true`, the value is scheduled to be
|
||||
* removed from the state when the patch is applied.
|
||||
*
|
||||
* @param value The value to check.
|
||||
* @returns `true` if the value is `None`, otherwise `false`.
|
||||
*/
|
||||
isNone: (value: unknown) => value is None;
|
||||
};
|
||||
|
||||
/**
|
||||
* A special value that denotes the absence of a value. Used to represent
|
||||
* removed values in patches.
|
||||
*/
|
||||
interface None {
|
||||
readonly __none: "__none";
|
||||
}
|
||||
|
||||
type MaybeNone<T> = undefined extends T ? None : never;
|
||||
|
||||
type DataTypes = {
|
||||
[P in keyof CheckableTypes as P extends keyof CheckablePrimitives ? never : P]: CheckableTypes[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* A type that should not be made partial in patches.
|
||||
*/
|
||||
type DataType = DataTypes[keyof DataTypes];
|
||||
|
||||
/**
|
||||
* A partial patch that can be applied to the state to update it. Represents
|
||||
* the difference between the current state and the next state.
|
||||
*
|
||||
* If a value was removed, it is replaced with `None`. This can be checked
|
||||
* using the `sync.isNone` function.
|
||||
*/
|
||||
type SyncPatch<State> =
|
||||
| MaybeNone<State>
|
||||
| (State extends ReadonlyMap<infer K, infer V> | Map<infer K, infer V>
|
||||
? ReadonlyMap<K, SyncPatch<V> | None>
|
||||
: State extends Set<infer T> | ReadonlySet<infer T>
|
||||
? ReadonlyMap<T, true | None>
|
||||
: State extends readonly (infer T)[]
|
||||
? readonly (SyncPatch<T> | None | undefined)[]
|
||||
: State extends DataType
|
||||
? State
|
||||
: State extends object
|
||||
? { readonly [P in keyof State]?: SyncPatch<State[P]> }
|
||||
: State);
|
||||
|
||||
/**
|
||||
* A payload that can be sent from the server to the client to synchronize
|
||||
* state between the two.
|
||||
*/
|
||||
type SyncPayload<Molecules extends MoleculeMap> =
|
||||
| { type: "init"; data: StateOfMap<Molecules> }
|
||||
| { type: "patch"; data: SyncPatch<StateOfMap<Molecules>> };
|
||||
|
||||
interface ClientOptions<Atoms extends AtomMap> {
|
||||
/**
|
||||
* The atoms to synchronize with the server.
|
||||
*/
|
||||
atoms: Atoms;
|
||||
/**
|
||||
* Whether to ignore patches sent before the client has been hydrated.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
ignoreUnhydrated?: boolean;
|
||||
}
|
||||
|
||||
interface ServerOptions<Molecules extends MoleculeMap> {
|
||||
/**
|
||||
* The atoms to synchronize with the client.
|
||||
*/
|
||||
atoms: Molecules;
|
||||
/**
|
||||
* The interval at which to send patches to the client, in seconds.
|
||||
* Defaults to `0` (patches are sent up to once per frame). Set to a
|
||||
* negative value to disable automatic syncing.
|
||||
*/
|
||||
interval?: number;
|
||||
/**
|
||||
* Whether the history of state changes since the client's last update
|
||||
* should be preserved. This is useful for values that change multiple times
|
||||
* per frame, where each individual change is important. Defaults to `false`.
|
||||
*
|
||||
* If `true`, the broadcaster will send a list of payloads to the client
|
||||
* instead of a single payload. The client will apply each payload in order
|
||||
* to reconstruct the state's changes over time.
|
||||
*/
|
||||
preserveHistory?: boolean;
|
||||
}
|
||||
|
||||
interface ClientSyncer<Atoms extends AtomMap> {
|
||||
/**
|
||||
* Applies a patch or initializes the state of the atoms with the given
|
||||
* payload from the server.
|
||||
*
|
||||
* @param payloads The patches or hydration payloads to apply.
|
||||
*/
|
||||
sync(...payloads: SyncPayload<Atoms>[]): void;
|
||||
}
|
||||
|
||||
interface ServerSyncer<Molecules extends MoleculeMap> {
|
||||
/**
|
||||
* Sets up a subscription to each atom that schedules a patch to be sent to
|
||||
* the client whenever the state changes. When a change occurs, the `callback`
|
||||
* is called with the player and the payloads to send.
|
||||
*
|
||||
* Note that a `payload` object should not be mutated. If you need to modify
|
||||
* a payload, apply the changes to a copy of the object.
|
||||
*
|
||||
* @param callback The function to call when the state changes.
|
||||
* @returns A cleanup function that unsubscribes all listeners.
|
||||
*/
|
||||
connect(callback: (player: Player, ...payloads: SyncPayload<Molecules>[]) => void): Cleanup;
|
||||
/**
|
||||
* Hydrates the client's state with the server's state. This should be
|
||||
* called when a player joins the game and requires the server's state.
|
||||
*
|
||||
* @param player The player to hydrate.
|
||||
*/
|
||||
hydrate(player: Player): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
_G[script.modules.Promise] = _G[script]
|
||||
|
||||
local atom = require(script.atom)
|
||||
local computed = require(script.computed)
|
||||
local effect = require(script.effect)
|
||||
local mapped = require(script.mapped)
|
||||
local observe = require(script.observe)
|
||||
local useAtom = require(script.react.useAtom)
|
||||
local store = require(script.store)
|
||||
local subscribe = require(script.subscribe)
|
||||
local sync = require(script.sync)
|
||||
local types = require(script.types)
|
||||
|
||||
export type Atom<State> = types.Atom<State>
|
||||
|
|
@ -21,12 +17,10 @@ return {
|
|||
effect = effect,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
useAtom = useAtom,
|
||||
subscribe = subscribe,
|
||||
batch = store.batch,
|
||||
capture = store.capture,
|
||||
isAtom = store.isAtom,
|
||||
notify = store.notify,
|
||||
peek = store.peek,
|
||||
subscribe = subscribe,
|
||||
sync = sync,
|
||||
}
|
||||
7
src/charm/wally.toml
Normal file
7
src/charm/wally.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "littensy/charm"
|
||||
version = "0.8.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
local Package = script.Parent.Parent
|
||||
|
||||
if _G[script] then
|
||||
return _G[script].Promise
|
||||
end
|
||||
|
||||
assert(Package.Parent:FindFirstChild("Promise"), `Could not find Promise in {Package.Parent:GetFullName()}`)
|
||||
|
||||
return require(Package.Parent.Promise)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
local Package = script.Parent.Parent
|
||||
local Index = script:FindFirstAncestor("_Index")
|
||||
local Packages = if Package.Parent:FindFirstChild("React")
|
||||
then Package.Parent
|
||||
elseif Index then Index.Parent
|
||||
else Package.Parent.Parent:FindFirstChild("ReactLua")
|
||||
|
||||
type React = typeof(require(Package.Parent.React))
|
||||
|
||||
local React: React
|
||||
|
||||
return function()
|
||||
if not React then
|
||||
React = Packages and require(Packages.React) :: React
|
||||
end
|
||||
return React
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
local Package = script.Parent.Parent
|
||||
local Packages = if Package.Parent:FindFirstChild("ReactRoblox")
|
||||
then Package.Parent
|
||||
else Package.Parent.Parent:FindFirstChild("ReactLua")
|
||||
|
||||
type ReactRoblox = typeof(require(Package.Parent.ReactRoblox))
|
||||
|
||||
-- It's safe to require this module since it's only used in unit tests.
|
||||
return (if Packages then require(Packages.ReactRoblox) else nil) :: ReactRoblox
|
||||
3
src/react/.luaurc
Normal file
3
src/react/.luaurc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"languageMode": "strict"
|
||||
}
|
||||
6
src/react/default.project.json
Normal file
6
src/react/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "react-charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
19
src/react/package.json
Normal file
19
src/react/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@rbxts/react-charm",
|
||||
"version": "0.1.0",
|
||||
"description": "React bindings for Charm",
|
||||
"repository": "littensy/charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/react": "*"
|
||||
}
|
||||
}
|
||||
15
src/react/src/index.d.ts
vendored
Normal file
15
src/react/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Molecule } from "@rbxts/charm";
|
||||
|
||||
/**
|
||||
* A hook that subscribes to changes in the given atom or molecule. The
|
||||
* component is re-rendered whenever the state changes.
|
||||
*
|
||||
* If the `dependencies` array is provided, the subscription to the atom or
|
||||
* molecule is re-created whenever the dependencies change. Otherwise, the
|
||||
* subscription is created once when the component is mounted.
|
||||
*
|
||||
* @param molecule The atom or molecule to subscribe to.
|
||||
* @param dependencies An array of values that the subscription depends on.
|
||||
* @returns The current state.
|
||||
*/
|
||||
export function useAtom<State>(molecule: Molecule<State>, dependencies?: unknown[]): State;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
local requireReact = require(script.Parent.Parent.modules.React)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Molecule<T> = types.Molecule<T>
|
||||
local Charm = require(script.Parent.Charm)
|
||||
local React = require(script.Parent.React)
|
||||
|
||||
--[=[
|
||||
A hook that subscribes to changes in the given atom or molecule. The
|
||||
|
|
@ -15,17 +13,17 @@ type Molecule<T> = types.Molecule<T>
|
|||
@param dependencies An array of values that the subscription depends on.
|
||||
@return The current state.
|
||||
]=]
|
||||
local function useAtom<State>(molecule: Molecule<State>, dependencies: { any }?): State
|
||||
local React = requireReact()
|
||||
|
||||
local function useAtom<State>(molecule: Charm.Molecule<State>, dependencies: { any }?): State
|
||||
local state, setState = React.useState(molecule)
|
||||
|
||||
React.useEffect(function()
|
||||
setState(molecule())
|
||||
return subscribe(molecule, setState)
|
||||
return Charm.subscribe(molecule, setState)
|
||||
end, dependencies or {})
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
return useAtom
|
||||
return {
|
||||
useAtom = useAtom,
|
||||
}
|
||||
7
src/react/wally.toml
Normal file
7
src/react/wally.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "littensy/react-charm"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
3
src/vide/.luaurc
Normal file
3
src/vide/.luaurc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"languageMode": "strict"
|
||||
}
|
||||
6
src/vide/default.project.json
Normal file
6
src/vide/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "vide-charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
19
src/vide/package.json
Normal file
19
src/vide/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@rbxts/vide-charm",
|
||||
"version": "0.1.0",
|
||||
"description": "Vide bindings for Charm",
|
||||
"repository": "littensy/charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/vide": "*"
|
||||
}
|
||||
}
|
||||
9
src/vide/src/index.d.ts
vendored
Normal file
9
src/vide/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Molecule } from "@rbxts/charm";
|
||||
|
||||
/**
|
||||
* Subscribes to the state of an atom and returns a Vide source.
|
||||
*
|
||||
* @param molecule The atom or molecule to subscribe to.
|
||||
* @returns The reactive source.
|
||||
*/
|
||||
export function useAtom<T>(atom: Molecule<T>): () => T;
|
||||
23
src/vide/src/init.luau
Normal file
23
src/vide/src/init.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local Vide = require(script.Parent.Vide)
|
||||
|
||||
--[=[
|
||||
Subscribes to the state of an atom and returns a Vide source.
|
||||
|
||||
@param molecule The atom or molecule to subscribe to.
|
||||
@return The reactive source.
|
||||
]=]
|
||||
local function useAtom<State>(molecule: () -> State): State
|
||||
local state = Vide.source(molecule())
|
||||
local unsubscribe = Charm.subscribe(molecule, function(value)
|
||||
task.spawn(state, value)
|
||||
end)
|
||||
|
||||
Vide.cleanup(unsubscribe)
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
return {
|
||||
useAtom = useAtom,
|
||||
}
|
||||
7
src/vide/wally.toml
Normal file
7
src/vide/wally.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "littensy/vide-charm"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -1 +1 @@
|
|||
roots = ["ReplicatedStorage/DevPackages/Charm/__tests__"]
|
||||
roots = ["ServerScriptService/Tests"]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
-- This file is for use by Benchmarker
|
||||
-- https://boatbomber.itch.io/benchmarker
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Package = game:GetService("ReplicatedStorage").DevPackages.Charm
|
||||
|
||||
local atom = require(Package.atom)
|
||||
local computed = require(Package.computed)
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local computed = Charm.computed
|
||||
local atom = Charm.atom
|
||||
|
||||
local function parameterGenerator()
|
||||
return atom(0)
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local subscribe = require(script.Parent.Parent.Parent.subscribe)
|
||||
local sync = require(script.Parent.Parent.Parent.sync)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
local client = CharmSync.client
|
||||
|
||||
return function()
|
||||
it("syncs state payload", function()
|
||||
local a = atom(1)
|
||||
local client = sync.client({ atoms = { a = a } })
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
|
|
@ -18,7 +22,7 @@ return function()
|
|||
it("syncs state patch", function()
|
||||
local a = atom({ b = 1, c = 2 })
|
||||
local previous = a()
|
||||
local client = sync.client({ atoms = { a = a } })
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
|
|
@ -39,7 +43,7 @@ return function()
|
|||
|
||||
it("fixes sparse array patches", function()
|
||||
local a = atom({ 1 })
|
||||
local client = sync.client({ atoms = { a = a } })
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
|
|
@ -60,7 +64,7 @@ return function()
|
|||
|
||||
it("applies multiple patches", function()
|
||||
local a = atom(1)
|
||||
local client = sync.client({ atoms = { a = a } })
|
||||
local client = client({ atoms = { a = a } })
|
||||
local calls = {}
|
||||
|
||||
subscribe(a, function(value)
|
||||
|
|
@ -88,7 +92,7 @@ return function()
|
|||
it("batches patches", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local client = sync.client({ atoms = { a = a, b = b } })
|
||||
local client = client({ atoms = { a = a, b = b } })
|
||||
local states = {}
|
||||
|
||||
subscribe(a, function()
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
local store = require(script.Parent.Parent.store)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
local previous = _G.__DEV__
|
||||
|
||||
beforeAll(function()
|
||||
_G.__DEV__ = true
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
_G.__DEV__ = previous
|
||||
_G.__DEV__ = nil
|
||||
end)
|
||||
|
||||
afterEach(function()
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
local HttpService = game:GetService("HttpService")
|
||||
local patch = require(script.Parent.Parent.Parent.sync.patch)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local function serdes(value: { [any]: any })
|
||||
|
|
@ -77,7 +79,7 @@ return function()
|
|||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
assert(os.clock() - start < 1e-3, `Performance is not acceptable ({(os.clock() - start) * 1e3}ms)`)
|
||||
assert(os.clock() - start < 10e-3, `Performance is not acceptable ({(os.clock() - start) * 1e3}ms)`)
|
||||
|
||||
for index in next, prevState do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local sync = require(script.Parent.Parent.Parent.sync)
|
||||
local patch = require(script.Parent.Parent.Parent.sync.patch)
|
||||
local types = require(script.Parent.Parent.Parent.types)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
type SyncPayload = CharmSync.SyncPayload
|
||||
local server = CharmSync.server
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local target: Player = {} :: any
|
||||
|
||||
it("sends state to new players", function()
|
||||
local a = atom(1)
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: types.SyncPayload
|
||||
local server = server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: SyncPayload
|
||||
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
|
|
@ -26,8 +30,8 @@ return function()
|
|||
|
||||
it("sends state patches", function()
|
||||
local a = atom({ b = 1, c = 2 } :: { [string]: number? })
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: types.SyncPayload
|
||||
local server = server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: SyncPayload
|
||||
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
|
|
@ -54,9 +58,9 @@ return function()
|
|||
it("preserves history", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local payloads: { types.SyncPayload }
|
||||
local payloads: { SyncPayload }
|
||||
|
||||
local server = sync.server({
|
||||
local server = server({
|
||||
atoms = { a = a, b = b },
|
||||
interval = -1,
|
||||
preserveHistory = true,
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local validate = require(script.Parent.Parent.Parent.sync.validate)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local validate = require(ReplicatedStorage.DevPackages.CharmSync.validate)
|
||||
|
||||
return function()
|
||||
it("validates inputs", function()
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("updates state", function()
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local computed = require(script.Parent.Parent.computed)
|
||||
local effect = require(script.Parent.Parent.effect)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local effect = Charm.effect
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("batches updates", function()
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("gracefully handles yielding", function()
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local computed = require(script.Parent.Parent.computed)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
local collect = require(script.Parent.Parent.utils.collect)
|
||||
local count = require(script.Parent.Parent.utils.count)
|
||||
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local effect = require(script.Parent.Parent.effect)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local effect = Charm.effect
|
||||
|
||||
return function()
|
||||
it("runs on state change", function()
|
||||
19
tests/charm/init.spec.luau
Normal file
19
tests/charm/init.spec.luau
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
beforeAll(function()
|
||||
_G.__DEV__ = true
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
_G.__DEV__ = nil
|
||||
end)
|
||||
|
||||
afterEach(function()
|
||||
table.clear(store.listeners :: any)
|
||||
table.clear(store.capturing.stack)
|
||||
store.capturing.index = 0
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local mapped = require(script.Parent.Parent.mapped)
|
||||
local observe = require(script.Parent.Parent.observe)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local mapped = Charm.mapped
|
||||
local observe = Charm.observe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("returns mapped atoms", function()
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local observe = require(script.Parent.Parent.observe)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local observe = Charm.observe
|
||||
|
||||
return function()
|
||||
it("observes lifetime", function()
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local capture = Charm.capture
|
||||
local peek = Charm.peek
|
||||
|
||||
return function()
|
||||
it("calls an atom without capturing it", function()
|
||||
local a, b, c = atom(1), atom(1), atom(1)
|
||||
local deps, result = store.capture(function()
|
||||
return a() + store.peek(b) + c()
|
||||
local deps, result = capture(function()
|
||||
return a() + peek(b) + c()
|
||||
end)
|
||||
expect(deps[a]).to.be.ok()
|
||||
expect(deps[b]).to.never.be.ok()
|
||||
|
|
@ -14,8 +18,8 @@ return function()
|
|||
end)
|
||||
|
||||
it("passes through arguments", function()
|
||||
expect(store.peek(1)).to.equal(1)
|
||||
expect(store.peek(function()
|
||||
expect(peek(1)).to.equal(1)
|
||||
expect(peek(function()
|
||||
return 1
|
||||
end)).to.equal(1)
|
||||
end)
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local computed = require(script.Parent.Parent.computed)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
local collect = require(script.Parent.Parent.utils.collect)
|
||||
local count = require(script.Parent.Parent.utils.count)
|
||||
|
||||
5
tests/init.server.luau
Normal file
5
tests/init.server.luau
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)
|
||||
|
||||
TestEZ.TestBootstrap:run({ script.Parent }, TestEZ.Reporters.TextReporter)
|
||||
|
|
@ -1,42 +1,46 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local useAtom = require(script.Parent.Parent.Parent.react.useAtom)
|
||||
local renderHook = require(script.Parent.helpers.renderHook)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local ReactCharm = require(ReplicatedStorage.DevPackages.ReactCharm)
|
||||
local useAtom = ReactCharm.useAtom
|
||||
local renderHook = require(script.Parent.Parent.utils.renderHook)
|
||||
|
||||
return function()
|
||||
it("tracks atom state", function()
|
||||
local source = atom(0)
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(source)
|
||||
return useAtom(state)
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
source(1)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
end)
|
||||
|
||||
it("accepts a molecule", function()
|
||||
local source = atom(0)
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(function()
|
||||
return 2 * source()
|
||||
return 2 * state()
|
||||
end)
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
source(1)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(2)
|
||||
end)
|
||||
|
||||
it("tracks dependencies", function()
|
||||
local source = atom(0)
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(source, { source })
|
||||
return useAtom(state, { state })
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
source(1)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
source = atom(0)
|
||||
state = atom(0)
|
||||
hook.rerender()
|
||||
expect(hook.result).to.equal(0)
|
||||
source(1)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||
|
||||
local Package = script:FindFirstAncestor("__tests__").Parent
|
||||
|
||||
local React = require(Package.modules.React)()
|
||||
local ReactRoblox = require(Package.modules.ReactRoblox)
|
||||
local React = require(ReplicatedStorage.DevPackages.React)
|
||||
local ReactRoblox = require(ReplicatedStorage.DevPackages.ReactRoblox)
|
||||
|
||||
type RenderedHook<Props, Result> = {
|
||||
result: Result,
|
||||
36
tests/vide/useAtom.spec.luau
Normal file
36
tests/vide/useAtom.spec.luau
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local Vide = require(ReplicatedStorage.DevPackages.Vide)
|
||||
local root = Vide.root
|
||||
local VideCharm = require(ReplicatedStorage.DevPackages.VideCharm)
|
||||
local useAtom = VideCharm.useAtom
|
||||
|
||||
return function()
|
||||
it("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
local source
|
||||
local cleanup = root(function()
|
||||
source = useAtom(state)
|
||||
end)
|
||||
expect(source()).to.equal(0)
|
||||
state(1)
|
||||
expect(source()).to.equal(1)
|
||||
cleanup()
|
||||
end)
|
||||
|
||||
it("accepts a molecule", function()
|
||||
local state = atom(0)
|
||||
local source
|
||||
local cleanup = root(function()
|
||||
source = useAtom(function()
|
||||
return 2 * state()
|
||||
end)
|
||||
end)
|
||||
expect(source()).to.equal(0)
|
||||
state(1)
|
||||
expect(source()).to.equal(2)
|
||||
cleanup()
|
||||
end)
|
||||
end
|
||||
24
wally.lock
24
wally.lock
|
|
@ -3,8 +3,8 @@
|
|||
registry = "test"
|
||||
|
||||
[[package]]
|
||||
name = "evaera/promise"
|
||||
version = "4.0.0"
|
||||
name = "centau/vide"
|
||||
version = "0.3.1"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
|
|
@ -54,27 +54,27 @@ dependencies = []
|
|||
|
||||
[[package]]
|
||||
name = "jsdotlua/react"
|
||||
version = "17.1.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["shared", "jsdotlua/shared@17.1.0"]]
|
||||
version = "17.2.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["shared", "jsdotlua/shared@17.2.0"]]
|
||||
|
||||
[[package]]
|
||||
name = "jsdotlua/react-reconciler"
|
||||
version = "17.1.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["promise", "jsdotlua/promise@3.5.2"], ["react", "jsdotlua/react@17.1.0"], ["scheduler", "jsdotlua/scheduler@17.1.0"], ["shared", "jsdotlua/shared@17.1.0"]]
|
||||
version = "17.2.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["promise", "jsdotlua/promise@3.5.2"], ["react", "jsdotlua/react@17.2.0"], ["scheduler", "jsdotlua/scheduler@17.2.0"], ["shared", "jsdotlua/shared@17.2.0"]]
|
||||
|
||||
[[package]]
|
||||
name = "jsdotlua/react-roblox"
|
||||
version = "17.1.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["react", "jsdotlua/react@17.1.0"], ["react-reconciler", "jsdotlua/react-reconciler@17.1.0"], ["scheduler", "jsdotlua/scheduler@17.1.0"], ["shared", "jsdotlua/shared@17.1.0"]]
|
||||
version = "17.2.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["react", "jsdotlua/react@17.2.0"], ["react-reconciler", "jsdotlua/react-reconciler@17.2.0"], ["scheduler", "jsdotlua/scheduler@17.2.0"], ["shared", "jsdotlua/shared@17.2.0"]]
|
||||
|
||||
[[package]]
|
||||
name = "jsdotlua/scheduler"
|
||||
version = "17.1.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["shared", "jsdotlua/shared@17.1.0"]]
|
||||
version = "17.2.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"], ["shared", "jsdotlua/shared@17.2.0"]]
|
||||
|
||||
[[package]]
|
||||
name = "jsdotlua/shared"
|
||||
version = "17.1.0"
|
||||
version = "17.2.0"
|
||||
dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.6"]]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -95,7 +95,7 @@ dependencies = [["collections", "jsdotlua/collections@1.2.6"]]
|
|||
[[package]]
|
||||
name = "littensy/charm"
|
||||
version = "0.1.0"
|
||||
dependencies = [["Promise", "evaera/promise@4.0.0"], ["React", "jsdotlua/react@17.1.0"], ["ReactRoblox", "jsdotlua/react-roblox@17.1.0"], ["TestEZ", "roblox/testez@0.4.1"]]
|
||||
dependencies = [["React", "jsdotlua/react@17.2.0"], ["ReactRoblox", "jsdotlua/react-roblox@17.2.0"], ["TestEZ", "roblox/testez@0.4.1"], ["Vide", "centau/vide@0.3.1"]]
|
||||
|
||||
[[package]]
|
||||
name = "roblox/testez"
|
||||
|
|
|
|||
17
wally.toml
17
wally.toml
|
|
@ -1,20 +1,11 @@
|
|||
[package]
|
||||
name = "littensy/charm"
|
||||
description = "Atomic state management for Roblox"
|
||||
version = "0.7.6"
|
||||
license = "MIT"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["**"]
|
||||
include = ["src", "src/**", "default.project.json", "wally.toml", "LICENSE.md"]
|
||||
|
||||
[place]
|
||||
shared-packages = "game.ReplicatedStorage.Packages"
|
||||
|
||||
[dependencies]
|
||||
Promise = "evaera/promise@4.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
React = "jsdotlua/react@17.1.0"
|
||||
ReactRoblox = "jsdotlua/react-roblox@17.1.0"
|
||||
React = "jsdotlua/react@17.2.0"
|
||||
ReactRoblox = "jsdotlua/react-roblox@17.2.0"
|
||||
TestEZ = "roblox/testez@0.4.1"
|
||||
Vide = "centau/vide@0.3.1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue