feat: add batch()

This commit is contained in:
Richard 2024-04-19 01:01:57 -07:00
parent 8a0d0977ca
commit f0a7938703
13 changed files with 112 additions and 52 deletions

View 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

View file

@ -1,35 +1,33 @@
local store = require(script.Parent.store) local store = require(script.Parent.store)
local types = require(script.Parent.types) local types = require(script.Parent.types)
local unwrap = require(script.Parent.unwrap)
type Atom<T> = types.Atom<T> type Atom<T> = types.Atom<T>
type AtomOptions<T> = types.AtomOptions<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 function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
local equals = options and options.equals local equals = options and options.equals
local function atom(...) local function atom(...)
if select("#", ...) == 0 then if select("#", ...) == 0 then
if store.shouldCapture() then if store.isCapturing() then
store.captured[atom] = true store.captured[atom] = true
end end
return state return state
end end
local nextState = ... -- Atoms called in a function should not be a part of the dependency tree
local nextState = unwrap(..., state)
if type(nextState) == "function" then
-- Atoms called in a function should not be included as dependencies
nextState = unwrap(function()
return nextState(state)
end)
end
if state ~= nextState and not (equals and equals(state, nextState)) then if state ~= nextState and not (equals and equals(state, nextState)) then
state = nextState state = nextState
for listener in pairs(table.clone(store.listeners[atom])) do if store.isBatching() then
listener() store.batched[atom] = true
else
for listener in pairs(table.clone(store.listeners[atom])) do
listener()
end
end end
end end

26
src/batch.luau Normal file
View 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

View file

@ -1,24 +1,19 @@
local store = require(script.Parent.store) local store = require(script.Parent.store)
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>
local function capture<T>(molecule: Molecule<T>): ({ [Atom<any>]: true }, T) local function capture<T>(callback: () -> T): ({ [Atom<any>]: true }, T)
if store.listeners[molecule] then if store.listeners[callback] then
return { [molecule] = true }, molecule() return { [callback] = true }, callback()
end end
store.setCapturing(true) store.setCapturing(true)
local success, result = pcall(molecule) local result = callback()
store.setCapturing(false) store.setCapturing(false)
local dependencies = table.clone(store.captured) local dependencies = table.clone(store.captured)
table.clear(store.captured) table.clear(store.captured)
if not success then
warn("An error occurred while calling an atom:", result)
end
return dependencies, result return dependencies, result
end end

6
src/index.d.ts vendored
View file

@ -28,12 +28,14 @@ declare namespace Charm {
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;
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 capture<State>(molecule: Molecule<State>): LuaTuple<[captured: Set<Atom<unknown>>, state: State]>;
function batch(callback: () => void): void;
function observe<Item>( function observe<Item>(
molecule: Molecule<readonly Item[]>, molecule: Molecule<readonly Item[]>,
factory: (item: Item, index: number) => Cleanup | void, factory: (item: Item, index: number) => Cleanup | void,

View file

@ -1,6 +1,7 @@
_G[script.modules.Promise] = _G[script] _G[script.modules.Promise] = _G[script]
local atom = require(script.atom) local atom = require(script.atom)
local batch = require(script.batch)
local capture = require(script.capture) local capture = require(script.capture)
local derive = require(script.derive) local derive = require(script.derive)
local effect = require(script.effect) local effect = require(script.effect)
@ -21,6 +22,7 @@ export type SyncPayload = types.SyncPayload
return { return {
atom = atom, atom = atom,
batch = batch,
capture = capture, capture = capture,
derive = derive, derive = derive,
effect = effect, effect = effect,

View file

@ -5,4 +5,5 @@ local Packages = if Package.Parent:FindFirstChild("ReactRoblox")
type ReactRoblox = typeof(require(Package.Parent.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 return (if Packages then require(Packages.ReactRoblox) else nil) :: ReactRoblox

View file

@ -1,6 +1,5 @@
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>
local useAtomState = require(script.Parent.useAtomState) local useAtomState = require(script.Parent.useAtomState)
local useSetAtom = require(script.Parent.useSetAtom) local useSetAtom = require(script.Parent.useSetAtom)

View file

@ -1,6 +1,5 @@
local requireReact = require(script.Parent.Parent.modules.React) local requireReact = require(script.Parent.Parent.modules.React)
local subscribe = require(script.Parent.Parent.subscribe) 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>

View file

@ -1,5 +1,4 @@
local requireReact = require(script.Parent.Parent.modules.React) local requireReact = require(script.Parent.Parent.modules.React)
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>

View file

@ -4,19 +4,32 @@ type WeakMap<K, V> = types.WeakMap<K, V>
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" }) local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
local captured: { [Atom<any>]: true } = {} local captured: { [Atom<any>]: true } = {}
local batched: { [Atom<any>]: true } = {}
local capturing = false local capturing = false
local batching = false
local function setCapturing(value: boolean) local function setCapturing(value: boolean)
capturing = value capturing = value
end end
local function shouldCapture() local function isCapturing()
return capturing return capturing
end end
local function setBatching(value: boolean)
batching = value
end
local function isBatching()
return batching
end
return { return {
listeners = listeners, listeners = listeners,
captured = captured, captured = captured,
batched = batched,
setCapturing = setCapturing, setCapturing = setCapturing,
shouldCapture = shouldCapture, isCapturing = isCapturing,
setBatching = setBatching,
isBatching = isBatching,
} }

View file

@ -2,8 +2,6 @@ local validate = require(script.Parent.validate)
local NONE = { __none = "__none" } local NONE = { __none = "__none" }
local warnings = {}
local function diff(prevState: { [any]: any }, nextState: { [any]: any }) local function diff(prevState: { [any]: any }, nextState: { [any]: any })
local patches = table.clone(nextState) local patches = table.clone(nextState)
@ -20,20 +18,14 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any })
end end
if _G.__DEV__ then 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) validate(value, key)
end 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
end end

View file

@ -1,22 +1,18 @@
local store = require(script.Parent.store) local store = require(script.Parent.store)
local function unwrap<T>(atom: (() -> T) | T): T local function unwrap<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
if type(atom) ~= "function" then if type(callback) ~= "function" then
return atom return callback
end end
if not store.shouldCapture() then if not store.isCapturing() then
return atom() return callback(...)
end end
store.setCapturing(false) store.setCapturing(false)
local success, result = pcall(atom) local result = callback(...)
store.setCapturing(true) store.setCapturing(true)
if not success then
error(`Failed to unwrap atom: ${result}`, 2)
end
return result return result
end end