mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor!: rename derive, map
This commit is contained in:
parent
f2f7a16599
commit
b715b81fec
18 changed files with 163 additions and 133 deletions
13
README.md
13
README.md
|
|
@ -18,18 +18,15 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Charm** is a Roblox state management library, inspired by [Jotai](https://jotai.org). Designed to be an alternative to [Reflex](https://littensy.github.io/reflex), Charm aims to bring an atomic and composable approach to your game's state.
|
**Charm** is an atomic state management library inspired by [Jotai](https://jotai.org). Designed to be a unique alternative to [Reflex](https://littensy.github.io/reflex), Charm aims to provide a simple and composable interface for your game's state.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Charm is incomplete and not ready for use.
|
> Charm is incomplete and not ready for production use.
|
||||||
|
|
||||||
## 📦 Setup
|
## 📦 Setup
|
||||||
|
|
||||||
### TypeScript
|
### TypeScript
|
||||||
|
|
||||||
Install Charm through your package manager of choice. \
|
|
||||||
[React](https://github.com/littensy/rbxts-react) is optional, but highly recommended.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install @rbxts/charm
|
npm install @rbxts/charm
|
||||||
yarn add @rbxts/charm
|
yarn add @rbxts/charm
|
||||||
|
|
@ -49,17 +46,17 @@ Charm = "littensy/charm@VERSION"
|
||||||
|
|
||||||
### Charm
|
### Charm
|
||||||
|
|
||||||
#### `atom(state)`
|
#### `atom(state, options?)`
|
||||||
|
|
||||||
#### `effect(callback)`
|
#### `effect(callback)`
|
||||||
|
|
||||||
#### `subscribe(atom, listener)`
|
#### `subscribe(atom, listener)`
|
||||||
|
|
||||||
#### `derive(atom, equals?)`
|
#### `computed(atom, options?)`
|
||||||
|
|
||||||
#### `observe(atom, observer)`
|
#### `observe(atom, observer)`
|
||||||
|
|
||||||
#### `map(atom, mapper)`
|
#### `mapped(atom, mapper)`
|
||||||
|
|
||||||
#### `peek(atom)`
|
#### `peek(atom)`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local atom = require(script.Parent.Parent.atom)
|
local atom = require(script.Parent.Parent.atom)
|
||||||
local derive = require(script.Parent.Parent.derive)
|
local computed = require(script.Parent.Parent.computed)
|
||||||
local effect = require(script.Parent.Parent.effect)
|
local effect = require(script.Parent.Parent.effect)
|
||||||
local store = require(script.Parent.Parent.store)
|
local store = require(script.Parent.Parent.store)
|
||||||
local subscribe = require(script.Parent.Parent.subscribe)
|
local subscribe = require(script.Parent.Parent.subscribe)
|
||||||
|
|
@ -37,15 +37,15 @@ return function()
|
||||||
expect(value).to.equal(1)
|
expect(value).to.equal(1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("batches derived atoms", function()
|
it("batches computed atoms", function()
|
||||||
local source = atom(1)
|
local source = atom(1)
|
||||||
local updates = 0
|
local updates = 0
|
||||||
local computed = 0
|
local computations = 0
|
||||||
local double = derive(function()
|
local double = computed(function()
|
||||||
computed += 1
|
computations += 1
|
||||||
return source() * 2
|
return source() * 2
|
||||||
end)
|
end)
|
||||||
computed = 0 -- derived atoms will run once initially
|
computations = 0 -- computed atoms will run once initially
|
||||||
subscribe(double, function()
|
subscribe(double, function()
|
||||||
updates += 1
|
updates += 1
|
||||||
end)
|
end)
|
||||||
|
|
@ -55,7 +55,7 @@ return function()
|
||||||
source(4)
|
source(4)
|
||||||
end)
|
end)
|
||||||
expect(updates).to.equal(1)
|
expect(updates).to.equal(1)
|
||||||
expect(computed).to.equal(1)
|
expect(computations).to.equal(1)
|
||||||
expect(double()).to.equal(8)
|
expect(double()).to.equal(8)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
local Package = game:GetService("ReplicatedStorage").Packages.charm
|
local Package = game:GetService("ReplicatedStorage").Packages.charm
|
||||||
|
|
||||||
local atom = require(Package.atom)
|
local atom = require(Package.atom)
|
||||||
local derive = require(Package.derive)
|
local computed = require(Package.computed)
|
||||||
|
|
||||||
local function parameterGenerator()
|
local function parameterGenerator()
|
||||||
return atom({ count = 0 })
|
return atom({ count = 0 })
|
||||||
|
|
@ -17,8 +17,8 @@ local benchmarks = {
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
derived = function(_, source: any)
|
computed = function(_, source: any)
|
||||||
derive(function()
|
computed(function()
|
||||||
return { count = 2 * source().count }
|
return { count = 2 * source().count }
|
||||||
end)
|
end)
|
||||||
for _ = 1, 500 do
|
for _ = 1, 500 do
|
||||||
|
|
@ -26,11 +26,11 @@ local benchmarks = {
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
derived_nested = function(_, source: any)
|
computed_nested = function(_, source: any)
|
||||||
local double = derive(function()
|
local double = computed(function()
|
||||||
return { count = 2 * source().count }
|
return { count = 2 * source().count }
|
||||||
end)
|
end)
|
||||||
derive(function()
|
computed(function()
|
||||||
return { count = 2 * double().count }
|
return { count = 2 * double().count }
|
||||||
end)
|
end)
|
||||||
for _ = 1, 500 do
|
for _ = 1, 500 do
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local atom = require(script.Parent.Parent.atom)
|
local atom = require(script.Parent.Parent.atom)
|
||||||
local derive = require(script.Parent.Parent.derive)
|
local computed = require(script.Parent.Parent.computed)
|
||||||
local store = require(script.Parent.Parent.store)
|
local store = require(script.Parent.Parent.store)
|
||||||
local collect = require(script.Parent.Parent.utils.collect)
|
local collect = require(script.Parent.Parent.utils.collect)
|
||||||
local count = require(script.Parent.Parent.utils.count)
|
local count = require(script.Parent.Parent.utils.count)
|
||||||
|
|
@ -9,7 +9,7 @@ return function()
|
||||||
local calls = 0
|
local calls = 0
|
||||||
local a = atom(1)
|
local a = atom(1)
|
||||||
local b = atom(2)
|
local b = atom(2)
|
||||||
local double = derive(function()
|
local double = computed(function()
|
||||||
calls += 1
|
calls += 1
|
||||||
return a() * b()
|
return a() * b()
|
||||||
end)
|
end)
|
||||||
|
|
@ -26,10 +26,10 @@ return function()
|
||||||
|
|
||||||
it("can be nested", function()
|
it("can be nested", function()
|
||||||
local source = atom(1)
|
local source = atom(1)
|
||||||
local double = derive(function()
|
local double = computed(function()
|
||||||
return source() * 2
|
return source() * 2
|
||||||
end)
|
end)
|
||||||
local quadruple = derive(function()
|
local quadruple = computed(function()
|
||||||
return double() * 2
|
return double() * 2
|
||||||
end)
|
end)
|
||||||
expect(quadruple()).to.equal(4)
|
expect(quadruple()).to.equal(4)
|
||||||
|
|
@ -37,34 +37,35 @@ return function()
|
||||||
expect(quadruple()).to.equal(8)
|
expect(quadruple()).to.equal(8)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("collects derived atoms", function()
|
it("collects computed atoms", function()
|
||||||
local source = atom(0)
|
local source = atom(0)
|
||||||
|
|
||||||
derive(function()
|
computed(function()
|
||||||
return source()
|
return source()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
collect()
|
collect()
|
||||||
|
|
||||||
assert(count(store.listeners[source]) == 0, "derived atom did not gc")
|
assert(count(store.listeners[source]) == 0, "computed atom did not gc")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("collects nested derived atoms", function()
|
it("collects nested computed atoms", function()
|
||||||
local source = atom(0)
|
local source = atom(0)
|
||||||
|
|
||||||
do
|
do
|
||||||
-- Declare this in another scope to ensure no references are kept
|
-- Declare this in another scope to ensure no references exist at the
|
||||||
local derived = derive(function()
|
-- time of garbage collection.
|
||||||
return source()
|
local double = computed(function()
|
||||||
|
return source() * 2
|
||||||
end)
|
end)
|
||||||
|
|
||||||
derive(function()
|
computed(function()
|
||||||
return derived() * 2
|
return double() * 2
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
collect()
|
collect()
|
||||||
|
|
||||||
assert(count(store.listeners[source]) == 0, "nested derived atom did not gc")
|
assert(count(store.listeners[source]) == 0, "nested computed atom did not gc")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
local atom = require(script.Parent.Parent.atom)
|
local atom = require(script.Parent.Parent.atom)
|
||||||
local map = require(script.Parent.Parent.map)
|
local mapped = require(script.Parent.Parent.mapped)
|
||||||
local observe = require(script.Parent.Parent.observe)
|
local observe = require(script.Parent.Parent.observe)
|
||||||
local store = require(script.Parent.Parent.store)
|
local store = require(script.Parent.Parent.store)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
it("returns mapped atoms", function()
|
it("returns mapped atoms", function()
|
||||||
local itemsAtom = atom({ "a", "b", "c" })
|
local itemsAtom = atom({ "a", "b", "c" })
|
||||||
local mappedAtom = map(itemsAtom, function(value, key)
|
local mappedAtom = mapped(itemsAtom, function(value, key)
|
||||||
return string.upper(value), key
|
return string.upper(value), key
|
||||||
end)
|
end)
|
||||||
local updates = 0
|
local updates = 0
|
||||||
|
|
@ -35,7 +35,7 @@ return function()
|
||||||
|
|
||||||
it("uses key extractor", function()
|
it("uses key extractor", function()
|
||||||
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
|
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
|
||||||
local mappedAtom = map(itemsAtom, function(item)
|
local mappedAtom = mapped(itemsAtom, function(item)
|
||||||
return item, item.key
|
return item, item.key
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ return function()
|
||||||
{ key = 3, value = "c" },
|
{ key = 3, value = "c" },
|
||||||
})
|
})
|
||||||
|
|
||||||
local mappedAtom = map(itemsAtom, function(item)
|
local mappedAtom = mapped(itemsAtom, function(item)
|
||||||
return item, item.key
|
return item, item.key
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local atom = require(script.Parent.Parent.atom)
|
local atom = require(script.Parent.Parent.atom)
|
||||||
local derive = require(script.Parent.Parent.derive)
|
local computed = require(script.Parent.Parent.computed)
|
||||||
local store = require(script.Parent.Parent.store)
|
local store = require(script.Parent.Parent.store)
|
||||||
local subscribe = require(script.Parent.Parent.subscribe)
|
local subscribe = require(script.Parent.Parent.subscribe)
|
||||||
local collect = require(script.Parent.Parent.utils.collect)
|
local collect = require(script.Parent.Parent.utils.collect)
|
||||||
|
|
@ -31,22 +31,22 @@ return function()
|
||||||
local updates = 0
|
local updates = 0
|
||||||
|
|
||||||
-- Create a new scope to encapsulate variables with a limited lifetime, like
|
-- Create a new scope to encapsulate variables with a limited lifetime, like
|
||||||
-- 'unsubscribe' and 'derived', to ensure they are garbage collected.
|
-- 'unsubscribe' and 'computed', to ensure they are garbage collected.
|
||||||
--
|
--
|
||||||
-- The idea here is to test whether subscribe() prevents every derived
|
-- The idea here is to test whether subscribe() prevents every computed
|
||||||
-- atom in a chain from being garbage collected. If any are GCed, then
|
-- atom in a chain from being garbage collected. If any are GCed, then
|
||||||
-- the subscription will stop updating and the test will fail.
|
-- the subscription will stop updating and the test will fail.
|
||||||
do
|
do
|
||||||
local unsubscribe
|
local unsubscribe
|
||||||
|
|
||||||
do
|
do
|
||||||
local derived = derive(function()
|
local double = computed(function()
|
||||||
return source()
|
return source() * 2
|
||||||
end)
|
end)
|
||||||
|
|
||||||
unsubscribe = subscribe(
|
unsubscribe = subscribe(
|
||||||
derive(function()
|
computed(function()
|
||||||
return derived() * 2
|
return double() * 2
|
||||||
end),
|
end),
|
||||||
function()
|
function()
|
||||||
updates += 1
|
updates += 1
|
||||||
|
|
@ -58,13 +58,13 @@ return function()
|
||||||
source(source() + 1)
|
source(source() + 1)
|
||||||
|
|
||||||
assert(updates == 1, "subscription stopped updating")
|
assert(updates == 1, "subscription stopped updating")
|
||||||
assert(count(store.listeners[source]) == 1, "derived atom was garbage collected")
|
assert(count(store.listeners[source]) == 1, "computed atom was garbage collected")
|
||||||
|
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
end
|
end
|
||||||
|
|
||||||
collect()
|
collect()
|
||||||
|
|
||||||
assert(count(store.listeners[source]) == 0, "derived atom did not collect")
|
assert(count(store.listeners[source]) == 0, "computed atom did not collect")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local atom = require(script.Parent.Parent.Parent.atom)
|
local atom = require(script.Parent.Parent.Parent.atom)
|
||||||
local sync = require(script.Parent.Parent.Parent.sync)
|
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 types = require(script.Parent.Parent.Parent.types)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
@ -24,7 +25,7 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("sends state patches", function()
|
it("sends state patches", function()
|
||||||
local a = atom({ b = 1, c = 2 })
|
local a = atom({ b = 1, c = 2 } :: { [string]: number? })
|
||||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||||
local player: Player, payload: types.SyncPayload
|
local player: Player, payload: types.SyncPayload
|
||||||
|
|
||||||
|
|
@ -33,13 +34,20 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
a({ b = 1, c = 5 })
|
a({ b = 1, c = 5 })
|
||||||
server:flush(target)
|
server:_sendPatch(target)
|
||||||
|
|
||||||
expect(player).to.equal(target)
|
expect(player).to.equal(target)
|
||||||
expect(payload.type).to.equal("patch")
|
expect(payload.type).to.equal("patch")
|
||||||
expect(payload.data.a.b).to.never.be.ok()
|
expect(payload.data.a.b).to.never.be.ok()
|
||||||
expect(payload.data.a.c).to.equal(5)
|
expect(payload.data.a.c).to.equal(5)
|
||||||
|
|
||||||
|
a({ b = 1, d = 1 })
|
||||||
|
server:_sendPatch(target)
|
||||||
|
|
||||||
|
expect(payload.data.a.b).to.never.be.ok()
|
||||||
|
expect(payload.data.a.c).to.equal(patch.NONE)
|
||||||
|
expect(payload.data.a.d).to.equal(1)
|
||||||
|
|
||||||
disconnect()
|
disconnect()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Atoms called in a function should not be a part of the dependency tree
|
|
||||||
local nextState = peek(..., state)
|
local nextState = peek(..., state)
|
||||||
|
|
||||||
if state ~= nextState and not (equals and equals(state, nextState)) then
|
if state ~= nextState and not (equals and equals(state, nextState)) then
|
||||||
|
|
|
||||||
26
src/computed.luau
Normal file
26
src/computed.luau
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
local atom = require(script.Parent.atom)
|
||||||
|
local store = require(script.Parent.store)
|
||||||
|
local types = require(script.Parent.types)
|
||||||
|
type Atom<T> = types.Atom<T>
|
||||||
|
type AtomOptions<T> = types.AtomOptions<T>
|
||||||
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
||||||
|
local function computed<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Molecule<T>
|
||||||
|
local captured, state = store.capture(molecule)
|
||||||
|
local computedAtom = atom(state, options)
|
||||||
|
local computedRef = setmetatable({ current = computedAtom }, { __mode = "v" })
|
||||||
|
|
||||||
|
local function listener()
|
||||||
|
if computedRef.current then
|
||||||
|
computedRef.current(molecule())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for atom in captured do
|
||||||
|
store.listeners[atom][listener] = computedAtom
|
||||||
|
end
|
||||||
|
|
||||||
|
return computedAtom
|
||||||
|
end
|
||||||
|
|
||||||
|
return computed
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
local atom = require(script.Parent.atom)
|
|
||||||
local store = require(script.Parent.store)
|
|
||||||
local types = require(script.Parent.types)
|
|
||||||
type Atom<T> = types.Atom<T>
|
|
||||||
type AtomOptions<T> = types.AtomOptions<T>
|
|
||||||
type Molecule<T> = types.Molecule<T>
|
|
||||||
|
|
||||||
local function derive<T>(molecule: Molecule<T>, options: AtomOptions<T>?): Molecule<T>
|
|
||||||
local atoms, state = store.capture(molecule)
|
|
||||||
local derived = atom(state, options)
|
|
||||||
local derivedRef = setmetatable({ current = derived }, { __mode = "v" })
|
|
||||||
|
|
||||||
local function listener()
|
|
||||||
if derivedRef.current then
|
|
||||||
derivedRef.current(molecule())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for atom in atoms do
|
|
||||||
store.listeners[atom][listener] = derived
|
|
||||||
end
|
|
||||||
|
|
||||||
return derived
|
|
||||||
end
|
|
||||||
|
|
||||||
return derive
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
local store = require(script.Parent.store)
|
local store = require(script.Parent.store)
|
||||||
|
|
||||||
local function effect(molecule: () -> nil): () -> ()
|
local function effect(molecule: () -> nil): () -> ()
|
||||||
local atoms = store.capture(molecule)
|
local captured = store.capture(molecule)
|
||||||
|
|
||||||
local function listener()
|
local function listener()
|
||||||
molecule()
|
molecule()
|
||||||
end
|
end
|
||||||
|
|
||||||
for atom in atoms do
|
for atom in captured do
|
||||||
store.listeners[atom][listener] = true
|
store.listeners[atom][listener] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
for atom in atoms do
|
for atom in captured do
|
||||||
store.listeners[atom][listener] = nil
|
store.listeners[atom][listener] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
8
src/index.d.ts
vendored
8
src/index.d.ts
vendored
|
|
@ -22,7 +22,7 @@ declare namespace Charm {
|
||||||
|
|
||||||
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
|
function atom<State>(state: State, options?: AtomOptions<State>): Atom<State>;
|
||||||
|
|
||||||
function derive<State>(molecule: Molecule<State>, options?: AtomOptions<State>): Molecule<State>;
|
function computed<State>(molecule: Molecule<State>, options?: AtomOptions<State>): Molecule<State>;
|
||||||
|
|
||||||
function subscribe<State>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
|
function subscribe<State>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
|
||||||
|
|
||||||
|
|
@ -48,17 +48,17 @@ declare namespace Charm {
|
||||||
factory: (item: Item, key: Key) => Cleanup | void,
|
factory: (item: Item, key: Key) => Cleanup | void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
function map<V0, K1, V1>(
|
function mapped<V0, K1, V1>(
|
||||||
molecule: Molecule<readonly V0[]>,
|
molecule: Molecule<readonly V0[]>,
|
||||||
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
mapper: (value: V0, index: number) => LuaTuple<[value: V1 | undefined, key: K1]>,
|
||||||
): Molecule<ReadonlyMap<K1, V1>>;
|
): Molecule<ReadonlyMap<K1, V1>>;
|
||||||
|
|
||||||
function map<V0, V1>(
|
function mapped<V0, V1>(
|
||||||
molecule: Molecule<readonly V0[]>,
|
molecule: Molecule<readonly V0[]>,
|
||||||
mapper: (value: V0, index: number) => V1,
|
mapper: (value: V0, index: number) => V1,
|
||||||
): Molecule<readonly V1[]>;
|
): Molecule<readonly V1[]>;
|
||||||
|
|
||||||
function map<K0, V0, K1 = K0, V1 = V0>(
|
function mapped<K0, V0, K1 = K0, V1 = V0>(
|
||||||
molecule: Molecule<AnyMap<K0, V0>>,
|
molecule: Molecule<AnyMap<K0, V0>>,
|
||||||
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
mapper: (value: V0, key: K0) => LuaTuple<[value: V1 | undefined, key: K1]> | V1,
|
||||||
): Molecule<ReadonlyMap<K1, V1>>;
|
): Molecule<ReadonlyMap<K1, V1>>;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
_G[script.modules.Promise] = _G[script]
|
_G[script.modules.Promise] = _G[script]
|
||||||
|
|
||||||
local atom = require(script.atom)
|
local atom = require(script.atom)
|
||||||
local derive = require(script.derive)
|
local computed = require(script.computed)
|
||||||
local effect = require(script.effect)
|
local effect = require(script.effect)
|
||||||
local map = require(script.map)
|
local mapped = require(script.mapped)
|
||||||
local observe = require(script.observe)
|
local observe = require(script.observe)
|
||||||
local peek = require(script.peek)
|
local peek = require(script.peek)
|
||||||
local useAtom = require(script.react.useAtom)
|
local useAtom = require(script.react.useAtom)
|
||||||
|
|
@ -18,9 +18,9 @@ export type SyncPayload = types.SyncPayload
|
||||||
|
|
||||||
return {
|
return {
|
||||||
atom = atom,
|
atom = atom,
|
||||||
derive = derive,
|
computed = computed,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
map = map,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
peek = peek,
|
peek = peek,
|
||||||
useAtom = useAtom,
|
useAtom = useAtom,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local atom = require(script.Parent.atom)
|
local atom = require(script.Parent.atom)
|
||||||
local subscribe = require(script.Parent.subscribe)
|
local effect = require(script.Parent.effect)
|
||||||
|
local peek = require(script.Parent.peek)
|
||||||
local types = require(script.Parent.types)
|
local types = require(script.Parent.types)
|
||||||
type Atom<T> = types.Atom<T>
|
type Atom<T> = types.Atom<T>
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
@ -9,18 +10,19 @@ type Map =
|
||||||
& (<K0, V0, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Molecule<{ [K0]: V1 }>)
|
& (<K0, V0, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> V1?) -> Molecule<{ [K0]: V1 }>)
|
||||||
& (<K0, V0, K1, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Molecule<{ [K1]: V1 }>)
|
& (<K0, V0, K1, V1>(fn: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)) -> Molecule<{ [K1]: V1 }>)
|
||||||
|
|
||||||
local function map<K0, V0, K1, V1>(getItems: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Molecule<{ [K1]: V1 }>
|
local function mapped<K0, V0, K1, V1>(molecule: Molecule<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): Molecule<{ [K1]: V1 }>
|
||||||
local mappedAtom = atom({})
|
local mappedAtom = atom({})
|
||||||
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
||||||
local prevMappedItems: { [K1]: V1 } = {}
|
local prevMappedItems: { [K1]: V1 } = {}
|
||||||
local unsubscribe
|
local unsubscribe
|
||||||
|
|
||||||
local function listener(items: { [K0]: V0 })
|
unsubscribe = effect(function()
|
||||||
if not mappedAtomRef.current then
|
if not mappedAtomRef.current then
|
||||||
return unsubscribe()
|
return unsubscribe()
|
||||||
end
|
end
|
||||||
|
|
||||||
local mappedItems = table.clone(mappedAtomRef.current())
|
local items = molecule()
|
||||||
|
local mappedItems = table.clone(peek(mappedAtomRef.current))
|
||||||
local mappedKeys = {}
|
local mappedKeys = {}
|
||||||
|
|
||||||
for key, item in items do
|
for key, item in items do
|
||||||
|
|
@ -43,12 +45,9 @@ local function map<K0, V0, K1, V1>(getItems: Molecule<{ [K0]: V0 }>, mapper: (V0
|
||||||
|
|
||||||
prevMappedItems = mappedItems
|
prevMappedItems = mappedItems
|
||||||
mappedAtom(mappedItems)
|
mappedAtom(mappedItems)
|
||||||
end
|
end)
|
||||||
|
|
||||||
unsubscribe = subscribe(getItems, listener)
|
|
||||||
listener(getItems())
|
|
||||||
|
|
||||||
return mappedAtom
|
return mappedAtom
|
||||||
end
|
end
|
||||||
|
|
||||||
return map :: Map
|
return mapped :: Map
|
||||||
|
|
@ -3,7 +3,7 @@ local types = require(script.Parent.types)
|
||||||
type Molecule<T> = types.Molecule<T>
|
type Molecule<T> = types.Molecule<T>
|
||||||
|
|
||||||
local function subscribe<T>(molecule: Molecule<T>, callback: (state: T, prev: T) -> ()): () -> ()
|
local function subscribe<T>(molecule: Molecule<T>, callback: (state: T, prev: T) -> ()): () -> ()
|
||||||
local atoms, state = store.capture(molecule)
|
local captured, state = store.capture(molecule)
|
||||||
|
|
||||||
local function listener()
|
local function listener()
|
||||||
local prevState = state
|
local prevState = state
|
||||||
|
|
@ -11,12 +11,12 @@ local function subscribe<T>(molecule: Molecule<T>, callback: (state: T, prev: T)
|
||||||
callback(state, prevState)
|
callback(state, prevState)
|
||||||
end
|
end
|
||||||
|
|
||||||
for atom in atoms do
|
for atom in captured do
|
||||||
store.listeners[atom][listener] = true
|
store.listeners[atom][listener] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
for atom in atoms do
|
for atom in captured do
|
||||||
store.listeners[atom][listener] = nil
|
store.listeners[atom][listener] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ local function client(options: ClientOptions): ClientSyncer
|
||||||
local atoms = options.atoms
|
local atoms = options.atoms
|
||||||
local self = {} :: ClientSyncer
|
local self = {} :: ClientSyncer
|
||||||
|
|
||||||
|
-- Apply the state changes sent by the server.
|
||||||
function self:sync(payload)
|
function self:sync(payload)
|
||||||
for key, state in payload.data do
|
for key, state in payload.data do
|
||||||
if payload.type == "patch" then
|
if payload.type == "patch" then
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ local function apply(state: any, patches: any): any
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
NONE = NONE,
|
||||||
diff = diff,
|
diff = diff,
|
||||||
apply = apply,
|
apply = apply,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local Players = game:GetService("Players")
|
local Players = game:GetService("Players")
|
||||||
|
|
||||||
|
local subscribe = require(script.Parent.Parent.subscribe)
|
||||||
local types = require(script.Parent.Parent.types)
|
local types = require(script.Parent.Parent.types)
|
||||||
type Atom<T> = types.Atom<T>
|
type Atom<T> = types.Atom<T>
|
||||||
type SyncPayload = types.SyncPayload
|
type SyncPayload = types.SyncPayload
|
||||||
|
|
@ -13,8 +14,8 @@ type ServerOptions = {
|
||||||
|
|
||||||
type ServerSyncer = {
|
type ServerSyncer = {
|
||||||
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
||||||
flush: (self: ServerSyncer, player: Player) -> (),
|
|
||||||
hydrate: (self: ServerSyncer, player: Player) -> (),
|
hydrate: (self: ServerSyncer, player: Player) -> (),
|
||||||
|
_sendPatch: (self: ServerSyncer, player: Player) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
local function server(options: ServerOptions): ServerSyncer
|
local function server(options: ServerOptions): ServerSyncer
|
||||||
|
|
@ -23,53 +24,76 @@ local function server(options: ServerOptions): ServerSyncer
|
||||||
|
|
||||||
local self = {} :: ServerSyncer
|
local self = {} :: ServerSyncer
|
||||||
local sync: (player: Player, payload: SyncPayload) -> ()
|
local sync: (player: Player, payload: SyncPayload) -> ()
|
||||||
local state: {}
|
local state: { [string]: any } = {}
|
||||||
|
local stateSnapshot = {}
|
||||||
local function getState()
|
local changed = false
|
||||||
local state = {}
|
|
||||||
for key, atom in atoms do
|
|
||||||
state[key] = atom()
|
|
||||||
end
|
|
||||||
return state
|
|
||||||
end
|
|
||||||
|
|
||||||
|
-- Start the interval to send state patches over the network.
|
||||||
function self:connect(callback)
|
function self:connect(callback)
|
||||||
|
local cleanups = {}
|
||||||
sync = callback
|
sync = callback
|
||||||
state = getState()
|
|
||||||
|
|
||||||
return setInterval(function()
|
-- Populate the initial state and snapshot for each atom.
|
||||||
local previous = state
|
-- Subscribe to each atom and update the state when it changes.
|
||||||
state = getState()
|
for key, atom in atoms do
|
||||||
|
cleanups[key] = subscribe(atom, function(current)
|
||||||
|
state[key] = current
|
||||||
|
changed = true
|
||||||
|
end)
|
||||||
|
|
||||||
if state ~= previous then
|
state[key] = atom()
|
||||||
local diffs = patch.diff(previous, state)
|
stateSnapshot[key] = atom()
|
||||||
|
end
|
||||||
|
|
||||||
for _, player in Players:GetPlayers() do
|
local disconnect = setInterval(function()
|
||||||
callback(player, {
|
if not changed then
|
||||||
type = "patch",
|
return
|
||||||
data = diffs,
|
end
|
||||||
})
|
|
||||||
end
|
local diffs = patch.diff(stateSnapshot, state)
|
||||||
|
stateSnapshot = table.clone(state)
|
||||||
|
changed = false
|
||||||
|
|
||||||
|
for _, player in Players:GetPlayers() do
|
||||||
|
callback(player, {
|
||||||
|
type = "patch",
|
||||||
|
data = diffs,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end, interval)
|
end, interval)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
disconnect()
|
||||||
|
for _, cleanup in cleanups do
|
||||||
|
cleanup()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function self:flush(player: Player)
|
-- Send the initial state to a player when they join the server.
|
||||||
local previous = state
|
|
||||||
state = getState()
|
|
||||||
|
|
||||||
sync(player, {
|
|
||||||
type = "patch",
|
|
||||||
data = patch.diff(previous, state),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function self:hydrate(player)
|
function self:hydrate(player)
|
||||||
assert(sync, "connect() must be called before hydrate()")
|
assert(sync, "connect() must be called before hydrate()")
|
||||||
|
|
||||||
sync(player, {
|
sync(player, {
|
||||||
type = "init",
|
type = "init",
|
||||||
data = getState(),
|
data = state,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function self:_sendPatch(player: Player)
|
||||||
|
assert(sync, "connect() must be called before _sendPatch()")
|
||||||
|
|
||||||
|
if not changed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local diffs = patch.diff(stateSnapshot, state)
|
||||||
|
stateSnapshot = table.clone(state)
|
||||||
|
changed = false
|
||||||
|
|
||||||
|
sync(player, {
|
||||||
|
type = "patch",
|
||||||
|
data = diffs,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue