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 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
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 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
View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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>

View file

@ -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>

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 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,
}

View file

@ -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

View file

@ -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