mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 03:36:11 +00:00
feat: add batch()
This commit is contained in:
parent
8a0d0977ca
commit
f0a7938703
13 changed files with 112 additions and 52 deletions
38
src/__tests__/batch.spec.luau
Normal file
38
src/__tests__/batch.spec.luau
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local batch = require(script.Parent.Parent.batch)
|
||||
local effect = require(script.Parent.Parent.effect)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
|
||||
return function()
|
||||
it("batches updates", function()
|
||||
local source = atom(1)
|
||||
local value = 0
|
||||
subscribe(source, function()
|
||||
value += 1
|
||||
end)
|
||||
batch(function()
|
||||
source(2)
|
||||
source(3)
|
||||
source(4)
|
||||
end)
|
||||
expect(value).to.equal(1)
|
||||
end)
|
||||
|
||||
it("handles duplicate listeners", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local value = 0
|
||||
effect(function()
|
||||
value += 1
|
||||
a()
|
||||
b()
|
||||
end)
|
||||
value = 0 -- effect() will run once initially
|
||||
batch(function()
|
||||
a(2)
|
||||
b(2)
|
||||
a(3)
|
||||
end)
|
||||
expect(value).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,35 +1,33 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
local unwrap = require(script.Parent.unwrap)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
local unwrap = require(script.Parent.unwrap)
|
||||
|
||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||
local equals = options and options.equals
|
||||
|
||||
local function atom(...)
|
||||
if select("#", ...) == 0 then
|
||||
if store.shouldCapture() then
|
||||
if store.isCapturing() then
|
||||
store.captured[atom] = true
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local nextState = ...
|
||||
|
||||
if type(nextState) == "function" then
|
||||
-- Atoms called in a function should not be included as dependencies
|
||||
nextState = unwrap(function()
|
||||
return nextState(state)
|
||||
end)
|
||||
end
|
||||
-- Atoms called in a function should not be a part of the dependency tree
|
||||
local nextState = unwrap(..., state)
|
||||
|
||||
if state ~= nextState and not (equals and equals(state, nextState)) then
|
||||
state = nextState
|
||||
|
||||
for listener in pairs(table.clone(store.listeners[atom])) do
|
||||
listener()
|
||||
if store.isBatching() then
|
||||
store.batched[atom] = true
|
||||
else
|
||||
for listener in pairs(table.clone(store.listeners[atom])) do
|
||||
listener()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
26
src/batch.luau
Normal file
26
src/batch.luau
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
local store = require(script.Parent.store)
|
||||
|
||||
local function batch(callback: () -> ())
|
||||
if store.isBatching() then
|
||||
return callback()
|
||||
end
|
||||
|
||||
local visited: { [() -> ()]: true } = {}
|
||||
|
||||
store.setBatching(true)
|
||||
callback()
|
||||
store.setBatching(false)
|
||||
|
||||
for atom in store.batched do
|
||||
for listener in pairs(store.listeners[atom]) do
|
||||
if not visited[listener] then
|
||||
visited[listener] = true
|
||||
listener()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.clear(store.batched)
|
||||
end
|
||||
|
||||
return batch
|
||||
|
|
@ -1,24 +1,19 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Molecule<T> = types.Molecule<T>
|
||||
|
||||
local function capture<T>(molecule: Molecule<T>): ({ [Atom<any>]: true }, T)
|
||||
if store.listeners[molecule] then
|
||||
return { [molecule] = true }, molecule()
|
||||
local function capture<T>(callback: () -> T): ({ [Atom<any>]: true }, T)
|
||||
if store.listeners[callback] then
|
||||
return { [callback] = true }, callback()
|
||||
end
|
||||
|
||||
store.setCapturing(true)
|
||||
local success, result = pcall(molecule)
|
||||
local result = callback()
|
||||
store.setCapturing(false)
|
||||
|
||||
local dependencies = table.clone(store.captured)
|
||||
table.clear(store.captured)
|
||||
|
||||
if not success then
|
||||
warn("An error occurred while calling an atom:", result)
|
||||
end
|
||||
|
||||
return dependencies, result
|
||||
end
|
||||
|
||||
|
|
|
|||
6
src/index.d.ts
vendored
6
src/index.d.ts
vendored
|
|
@ -28,12 +28,14 @@ declare namespace Charm {
|
|||
|
||||
function subscribe<State>(molecule: Molecule<State>, callback: (state: State, prev: State) => void): Cleanup;
|
||||
|
||||
function effect(callback: Molecule<void>): Cleanup;
|
||||
function effect(callback: () => void): Cleanup;
|
||||
|
||||
function unwrap<State>(molecule: State | Molecule<State>): State;
|
||||
function unwrap<State, Args extends unknown[]>(molecule: State | ((...args: Args) => State), ...args: Args): State;
|
||||
|
||||
function capture<State>(molecule: Molecule<State>): LuaTuple<[captured: Set<Atom<unknown>>, state: State]>;
|
||||
|
||||
function batch(callback: () => void): void;
|
||||
|
||||
function observe<Item>(
|
||||
molecule: Molecule<readonly Item[]>,
|
||||
factory: (item: Item, index: number) => Cleanup | void,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
_G[script.modules.Promise] = _G[script]
|
||||
|
||||
local atom = require(script.atom)
|
||||
local batch = require(script.batch)
|
||||
local capture = require(script.capture)
|
||||
local derive = require(script.derive)
|
||||
local effect = require(script.effect)
|
||||
|
|
@ -21,6 +22,7 @@ export type SyncPayload = types.SyncPayload
|
|||
|
||||
return {
|
||||
atom = atom,
|
||||
batch = batch,
|
||||
capture = capture,
|
||||
derive = derive,
|
||||
effect = effect,
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ local Packages = if Package.Parent:FindFirstChild("ReactRoblox")
|
|||
|
||||
type ReactRoblox = typeof(require(Package.Parent.ReactRoblox))
|
||||
|
||||
-- [INFO] 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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
local useAtomState = require(script.Parent.useAtomState)
|
||||
local useSetAtom = require(script.Parent.useSetAtom)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +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 Atom<T> = types.Atom<T>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
local requireReact = require(script.Parent.Parent.modules.React)
|
||||
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,32 @@ type WeakMap<K, V> = types.WeakMap<K, V>
|
|||
|
||||
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
|
||||
local captured: { [Atom<any>]: true } = {}
|
||||
local batched: { [Atom<any>]: true } = {}
|
||||
local capturing = false
|
||||
local batching = false
|
||||
|
||||
local function setCapturing(value: boolean)
|
||||
capturing = value
|
||||
end
|
||||
|
||||
local function shouldCapture()
|
||||
local function isCapturing()
|
||||
return capturing
|
||||
end
|
||||
|
||||
local function setBatching(value: boolean)
|
||||
batching = value
|
||||
end
|
||||
|
||||
local function isBatching()
|
||||
return batching
|
||||
end
|
||||
|
||||
return {
|
||||
listeners = listeners,
|
||||
captured = captured,
|
||||
batched = batched,
|
||||
setCapturing = setCapturing,
|
||||
shouldCapture = shouldCapture,
|
||||
isCapturing = isCapturing,
|
||||
setBatching = setBatching,
|
||||
isBatching = isBatching,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ local validate = require(script.Parent.validate)
|
|||
|
||||
local NONE = { __none = "__none" }
|
||||
|
||||
local warnings = {}
|
||||
|
||||
local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
||||
local patches = table.clone(nextState)
|
||||
|
||||
|
|
@ -20,20 +18,14 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any })
|
|||
end
|
||||
|
||||
if _G.__DEV__ then
|
||||
local success, result = pcall(function()
|
||||
for key, value in prevState do
|
||||
for key, value in prevState do
|
||||
validate(value, key)
|
||||
end
|
||||
|
||||
for key, value in nextState do
|
||||
if prevState[key] ~= value then
|
||||
validate(value, key)
|
||||
end
|
||||
for key, value in nextState do
|
||||
if prevState[key] ~= value then
|
||||
validate(value, key)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if not success and not warnings[result] then
|
||||
warnings[result] = true
|
||||
warn(result)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
local store = require(script.Parent.store)
|
||||
|
||||
local function unwrap<T>(atom: (() -> T) | T): T
|
||||
if type(atom) ~= "function" then
|
||||
return atom
|
||||
local function unwrap<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
|
||||
if type(callback) ~= "function" then
|
||||
return callback
|
||||
end
|
||||
|
||||
if not store.shouldCapture() then
|
||||
return atom()
|
||||
if not store.isCapturing() then
|
||||
return callback(...)
|
||||
end
|
||||
|
||||
store.setCapturing(false)
|
||||
local success, result = pcall(atom)
|
||||
local result = callback(...)
|
||||
store.setCapturing(true)
|
||||
|
||||
if not success then
|
||||
error(`Failed to unwrap atom: ${result}`, 2)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue