mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 04:14:08 +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 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
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 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
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 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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue