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,37 +1,35 @@
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
if store.isBatching() then
store.batched[atom] = true
else
for listener in pairs(table.clone(store.listeners[atom])) do for listener in pairs(table.clone(store.listeners[atom])) do
listener() listener()
end end
end end
end
return state return state
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,21 +18,15 @@ 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) validate(value, key)
end end
for key, value in nextState do for key, value in nextState do
if prevState[key] ~= value then if prevState[key] ~= value then
validate(value, key) validate(value, key)
end end
end end
end)
if not success and not warnings[result] then
warnings[result] = true
warn(result)
end
end end
return patches return patches

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