feat!: replace split with mapAtom

This commit is contained in:
Richard 2024-03-03 22:50:59 -08:00
parent fc3cd6d9ad
commit 4d4979daa9
11 changed files with 188 additions and 245 deletions

View file

@ -57,7 +57,7 @@ Charm = "littensy/charm@VERSION"
#### `observe(atom, observer)` #### `observe(atom, observer)`
#### `split(atom, keyExtractor?)` #### `mapAtom(atom, mapper)`
### Atom ### Atom
@ -67,7 +67,7 @@ Charm = "littensy/charm@VERSION"
#### `Atom.set(state)` #### `Atom.set(state)`
#### `Atom.memo(equalityFn)` #### `Atom.memo(areEqual)`
#### `Atom.readonly()` #### `Atom.readonly()`

View file

@ -0,0 +1,105 @@
local atom = require(script.Parent.Parent.atom)
local mapAtom = require(script.Parent.Parent.mapAtom)
local observe = require(script.Parent.Parent.observe)
return function()
it("returns mapped atoms", function()
local itemsAtom = atom({ "a", "b", "c" })
local mappedAtom = mapAtom(itemsAtom, function(value, key)
return string.upper(value), key
end)
local updates = 0
mappedAtom.listeners[function()
updates += 1
end] = true
for key = 1, 3 do
local item = mappedAtom()[key]
expect(item).to.equal(string.upper(itemsAtom()[key]))
end
itemsAtom({ "d", "e", "f" })
expect(updates).to.equal(1)
for key = 1, 3 do
local item = mappedAtom()[key]
expect(item).to.equal(string.upper(itemsAtom()[key]))
end
itemsAtom({ [1] = "d", [3] = "f" })
expect(mappedAtom()[2]).to.never.be.ok()
expect(updates).to.equal(2)
end)
it("uses key extractor", function()
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
local mappedAtom = mapAtom(itemsAtom, function(item)
return item, item.key
end)
for key = 1, 3 do
local item = mappedAtom()[key]
expect(item).to.equal(itemsAtom()[key])
end
local previous = mappedAtom()
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
expect(mappedAtom()[1]).to.equal(previous[1])
expect(mappedAtom()[1]).to.equal(itemsAtom()[1])
expect(mappedAtom()[2]).to.never.be.ok()
expect(mappedAtom()[3]).to.equal(previous[3])
expect(mappedAtom()[3]).to.equal(itemsAtom()[2])
end)
it("allows observe()", function()
local itemsAtom = atom({
{ key = 1, value = "a" },
{ key = 2, value = "b" },
{ key = 3, value = "c" },
})
local mappedAtom = mapAtom(itemsAtom, function(item)
return item, item.key
end)
local added = 0
local deleted = 0
local _, flush = observe(mappedAtom, function(item, key)
expect(item).to.equal(itemsAtom()[key])
added += 1
return function()
expect(mappedAtom()[key]).to.never.be.ok()
deleted += 1
end
end)
expect(added).to.equal(3)
expect(deleted).to.equal(0)
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
flush()
expect(added).to.equal(3)
expect(deleted).to.equal(1)
itemsAtom({})
flush()
expect(added).to.equal(3)
expect(deleted).to.equal(3)
end)
it("unmounts from parent atom", function()
local itemsAtom = atom({})
local mappedAtom = mapAtom(itemsAtom, function(item, key)
return item, key
end)
expect((next(itemsAtom.listeners))).to.be.ok()
mappedAtom:unmount()
expect((next(itemsAtom.listeners))).to.never.be.ok()
end)
end

View file

@ -1,136 +0,0 @@
local atom = require(script.Parent.Parent.atom)
local isAtom = require(script.Parent.Parent.isAtom)
local observe = require(script.Parent.Parent.observe)
local split = require(script.Parent.Parent.split)
return function()
it("returns split atoms", function()
local itemsAtom = atom({ "a", "b", "c" })
local itemAtoms = split(itemsAtom)
local updates = 0
itemAtoms.listeners[function()
updates += 1
end] = true
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
assert(isAtom(itemAtom), `itemAtom {key} is not an atom; got {typeof(itemAtom)}`)
expect(itemAtom()).to.equal(itemsAtom()[key])
end
local prevAtoms = itemAtoms()
itemsAtom({ "d", "e", "f" })
expect(updates).to.equal(1)
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
assert(itemAtom == prevAtoms[key] :: any, `itemAtom {key} is not the same atom; got {typeof(itemAtom)}`)
expect(itemAtom()).to.equal(itemsAtom()[key])
end
itemsAtom({ [1] = "d", [3] = "f" })
expect(itemAtoms()[2]).to.never.be.ok()
expect(updates).to.equal(2)
end)
it("has writable items", function()
local itemsAtom = atom({ "a", "b", "c" })
local itemAtoms = split(itemsAtom)
local updates = 0
itemAtoms.listeners[function()
updates += 1
end] = true
itemAtoms()[2]("x")
expect(updates).to.equal(1)
expect(itemsAtom()[2]).to.equal("x")
expect(itemAtoms()[2]()).to.equal("x")
end)
it("uses key extractor", function()
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
local itemAtoms = split(itemsAtom, function(item)
return item.key
end)
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
expect(itemAtom()).to.equal(itemsAtom()[key])
end
local prevAtoms = itemAtoms()
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
expect(itemAtoms()[1]).to.equal(prevAtoms[1])
expect(itemAtoms()[1]()).to.equal(itemsAtom()[1])
expect(itemAtoms()[2]).to.never.be.ok()
expect(itemAtoms()[3]).to.equal(prevAtoms[3])
expect(itemAtoms()[3]()).to.equal(itemsAtom()[2])
end)
it("allows observe()", function()
local itemsAtom = atom({
{ key = 1, value = "a" },
{ key = 2, value = "b" },
{ key = 3, value = "c" },
})
local itemAtoms = split(itemsAtom, function(item)
return item.key
end)
local added = 0
local updated = 0
local deleted = 0
local _, flush = observe(itemAtoms, function(item, key)
expect(item()).to.equal(itemsAtom()[key])
item.listeners[function(value)
expect(value).to.equal(itemsAtom()[key])
updated += 1
end] = true
added += 1
return function()
expect(itemAtoms()[key]).to.never.be.ok()
deleted += 1
end
end)
expect(added).to.equal(3)
expect(updated).to.equal(0)
expect(deleted).to.equal(0)
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
flush()
expect(added).to.equal(3)
expect(updated).to.equal(0)
expect(deleted).to.equal(1)
itemAtoms()[1]({ key = 1, value = "x" })
flush()
expect(added).to.equal(3)
expect(updated).to.equal(1)
expect(deleted).to.equal(1)
itemsAtom({})
flush()
expect(added).to.equal(3)
expect(updated).to.equal(1)
expect(deleted).to.equal(3)
end)
it("unmounts from parent atom", function()
local itemsAtom = atom({})
local itemAtoms = split(itemsAtom)
expect((next(itemsAtom.listeners))).to.be.ok()
itemAtoms:unmount()
expect((next(itemsAtom.listeners))).to.never.be.ok()
end)
end

View file

@ -11,7 +11,7 @@ type AtomInternal<T> = {
export type Atom<T> = (() -> T) & ((state: T | ((T) -> T)) -> ()) & { export type Atom<T> = (() -> T) & ((state: T | ((T) -> T)) -> ()) & {
get: ((self: Atom<T>) -> T) & (<Result>(self: Atom<T>, selector: (T) -> Result) -> Result), get: ((self: Atom<T>) -> T) & (<Result>(self: Atom<T>, selector: (T) -> Result) -> Result),
set: (self: Atom<T>, state: T | ((T) -> T)) -> (), set: (self: Atom<T>, state: T | ((T) -> T)) -> (),
memo: (self: Atom<T>, equalityFn: (previous: T, current: T) -> boolean) -> Atom<T>, memo: (self: Atom<T>, areEqual: (previous: T, current: T) -> boolean) -> Atom<T>,
writable: (self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>, writable: (self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>,
readonly: (self: Atom<T>) -> ReadonlyAtom<T>, readonly: (self: Atom<T>) -> ReadonlyAtom<T>,
named: (self: Atom<T>, label: string) -> Atom<T>, named: (self: Atom<T>, label: string) -> Atom<T>,
@ -20,7 +20,7 @@ export type Atom<T> = (() -> T) & ((state: T | ((T) -> T)) -> ()) & {
export type ReadonlyAtom<T> = (() -> T) & { export type ReadonlyAtom<T> = (() -> T) & {
get: ((self: ReadonlyAtom<T>) -> T) & (<Result>(self: ReadonlyAtom<T>, selector: (T) -> Result) -> Result), get: ((self: ReadonlyAtom<T>) -> T) & (<Result>(self: ReadonlyAtom<T>, selector: (T) -> Result) -> Result),
memo: (self: ReadonlyAtom<T>, equalityFn: (previous: T, current: T) -> boolean) -> ReadonlyAtom<T>, memo: (self: ReadonlyAtom<T>, areEqual: (previous: T, current: T) -> boolean) -> ReadonlyAtom<T>,
writable: ((self: Atom<T>) -> Atom<T>) writable: ((self: Atom<T>) -> Atom<T>)
& ((self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>), & ((self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>),
readonly: (self: ReadonlyAtom<T>) -> ReadonlyAtom<T>, readonly: (self: ReadonlyAtom<T>) -> ReadonlyAtom<T>,
@ -81,8 +81,8 @@ local function atom<State>(state: State): Atom<State>
end end
end end
function atom:memo(equalityFn: (State, State) -> boolean): Atom<State> function atom:memo(areEqual: (State, State) -> boolean): Atom<State>
equals = equalityFn equals = areEqual
return self return self
end end

View file

@ -1,7 +1,6 @@
local atom = require(script.Parent.atom) local atom = require(script.Parent.atom)
type Atom<T> = atom.Atom<T> type Atom<T> = atom.Atom<T>
type ReadonlyAtom<T> = atom.ReadonlyAtom<T> type ReadonlyAtom<T> = atom.ReadonlyAtom<T>
local reference = require(script.Parent.utils.reference) local reference = require(script.Parent.utils.reference)
type Derive = type Derive =
@ -29,7 +28,7 @@ local function derive<Result>(...): ReadonlyAtom<Result>
local getDerivedAtom = reference.weak(derivedAtom) local getDerivedAtom = reference.weak(derivedAtom)
function derivedAtom:unmount() function derivedAtom:unmount()
derivedAtomUnmount(derivedAtom) derivedAtomUnmount(self)
for listener in listeners do for listener in listeners do
for _, atom in atoms do for _, atom in atoms do

31
src/index.d.ts vendored
View file

@ -17,7 +17,7 @@ declare namespace Charm {
get(): State; get(): State;
get<Result>(selector: (state: State) => Result): Result; get<Result>(selector: (state: State) => Result): Result;
set(state: State | ((current: State) => State)): void; set(state: State | ((current: State) => State)): void;
memo(equalityFn: (previous: State, current: State) => boolean): this; memo(areEqual: (previous: State, current: State) => boolean): this;
writable(onSet?: (update: State, previous: State) => State | void): Atom<State>; writable(onSet?: (update: State, previous: State) => State | void): Atom<State>;
readonly(): ReadonlyAtom<State>; readonly(): ReadonlyAtom<State>;
named(label: string): this; named(label: string): this;
@ -55,22 +55,25 @@ declare namespace Charm {
function observe(atom: Atom<any>, factory: (value: unknown, key: unknown) => (() => void) | void): () => void; function observe(atom: Atom<any>, factory: (value: unknown, key: unknown) => (() => void) | void): () => void;
function splitAtom<Item, Key, Id = Key>( function mapAtom<V0, K1, V1>(
listAtom: Atom<ReadonlyMap<Key, Item>> | Atom<Map<Key, Item>>, atom: Atom<readonly V0[]> | Atom<V0[]>,
keyExtractor?: (item: Item) => Id, mapper: (value: V0, index: number) => LuaTuple<[V1 | undefined, K1]>,
): Atom<ReadonlyMap<Id, Atom<Item>>>; ): ReadonlyAtom<ReadonlyMap<K1, V1>>;
function splitAtom<Item>(listAtom: Atom<readonly Item[]>): Atom<readonly Atom<Item>[]>; function mapAtom<V0, V1>(
atom: Atom<readonly V0[]> | Atom<V0[]>,
mapper: (value: V0, index: number) => V1,
): ReadonlyAtom<readonly V1[]>;
function splitAtom<Item, Id = number>( function mapAtom<K0, V0, K1 = K0, V1 = V0>(
listAtom: Atom<readonly Item[]>, atom: Atom<ReadonlyMap<K0, V0>> | Atom<Map<K0, V0>>,
keyExtractor?: (item: Item) => Id, mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
): Atom<ReadonlyMap<Id, Atom<Item>>>; ): ReadonlyAtom<ReadonlyMap<K1, V1>>;
function splitAtom<Item, Key extends string | number | symbol, Id = Key>( function mapAtom<K0 extends string | number | symbol, V0, K1, V1>(
listAtom: Atom<{ readonly [K in Key]?: Item }>, atom: Atom<{ readonly [K in K0]: V0 }>,
keyExtractor?: (item: Item) => Id, mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
): Atom<ReadonlyMap<Id, Atom<Item>>>; ): ReadonlyAtom<ReadonlyMap<K1, V1>>;
function useAtomState<State>(atom: Atom<State>): State; function useAtomState<State>(atom: Atom<State>): State;

View file

@ -3,12 +3,12 @@ _G[script.modules.Promise] = _G[script]
local atom = require(script.atom) local atom = require(script.atom)
local derive = require(script.derive) local derive = require(script.derive)
local isAtom = require(script.isAtom) local isAtom = require(script.isAtom)
local mapAtom = require(script.mapAtom)
local observe = require(script.observe) local observe = require(script.observe)
local split = require(script.split)
local subscribe = require(script.subscribe)
local useAtom = require(script.react.useAtom) local useAtom = require(script.react.useAtom)
local useAtomState = require(script.react.useAtomState) local useAtomState = require(script.react.useAtomState)
local useSetAtom = require(script.react.useSetAtom) local useSetAtom = require(script.react.useSetAtom)
local subscribe = require(script.subscribe)
local sync = require(script.sync) local sync = require(script.sync)
export type Atom<State> = atom.Atom<State> export type Atom<State> = atom.Atom<State>
@ -19,7 +19,7 @@ return {
derive = derive, derive = derive,
isAtom = isAtom, isAtom = isAtom,
observe = observe, observe = observe,
split = split, mapAtom = mapAtom,
subscribe = subscribe, subscribe = subscribe,
sync = sync, sync = sync,
useAtom = useAtom, useAtom = useAtom,

56
src/mapAtom.luau Normal file
View file

@ -0,0 +1,56 @@
local atom = require(script.Parent.atom)
type Atom<T> = atom.Atom<T>
type ReadonlyAtom<T> = atom.ReadonlyAtom<T>
local reference = require(script.Parent.utils.reference)
local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): ReadonlyAtom<{ [K1]: V1 }>
local mappedAtom: ReadonlyAtom<{ [K1]: V1 }> = atom({}):readonly()
local mappedAtomUnmount = mappedAtom.unmount
local getMappedAtom = reference.weak(mappedAtom)
local previousMappedItems: { [K1]: V1 } = {}
local function listener(items)
local mappedAtom = getMappedAtom()
if not mappedAtom then
itemsAtom.listeners[listener] = nil
return
end
local mappedItems = table.clone(mappedAtom:get())
local mappedKeys = {}
for key, item in items do
local newItem, newKey = mapper(item, key)
if newKey == nil then
newKey = key :: any
end
if mappedItems[newKey :: K1] ~= newItem then
mappedItems[newKey :: K1] = newItem :: V1
else
mappedKeys[newKey] = key
end
end
for key in previousMappedItems do
if mappedKeys[key] == nil and mappedItems[key] == previousMappedItems[key] then
mappedItems[key] = nil
end
end
previousMappedItems = mappedItems
mappedAtom:setForced(mappedItems)
end
function mappedAtom:unmount()
mappedAtomUnmount(self)
itemsAtom.listeners[listener] = nil
end
itemsAtom.listeners[listener] = true
listener(itemsAtom:get())
return mappedAtom
end
return mapAtom

View file

@ -1,6 +1,5 @@
local atom = require(script.Parent.atom) local atom = require(script.Parent.atom)
type Atom<T> = atom.Atom<T> type Atom<T> = atom.Atom<T>
local subscribe = require(script.Parent.subscribe) local subscribe = require(script.Parent.subscribe)
local function observe<Item, Key>(atom: Atom<{ [Key]: Item }>, factory: (value: Item, key: Key) -> () -> ()): (() -> (), () -> ()) local function observe<Item, Key>(atom: Atom<{ [Key]: Item }>, factory: (value: Item, key: Key) -> () -> ()): (() -> (), () -> ())

View file

@ -1,82 +0,0 @@
local atom = require(script.Parent.atom)
type Atom<T> = atom.Atom<T>
type ReadonlyAtom<T> = atom.ReadonlyAtom<T>
local reference = require(script.Parent.utils.reference)
type Split =
(<Item, Key>(atom: Atom<{ [Key]: Item }>) -> ReadonlyAtom<{ [Key]: Atom<Item> }>)
& (<Item, Key>(atom: Atom<{ [any]: Item }>, keyExtractor: (Item) -> Key) -> ReadonlyAtom<{ [Key]: Atom<Item> }>)
local function split<Item, Key>(
itemsAtom: Atom<{ [Key]: Item }>,
keyExtractor: ((Item) -> Key)?
): ReadonlyAtom<{ [Key]: Atom<Item> }>
local splitAtom: ReadonlyAtom<{ [Key]: Atom<Item> }> = atom({}):readonly()
local splitAtomUnmount = splitAtom.unmount
local getSplitAtom = reference.weak(splitAtom)
local previousAtoms: { [Key]: Atom<Item> } = {}
local previousItems = itemsAtom:get()
local listener: ({ [Key]: Item }) -> ()
local function cleanup()
itemsAtom.listeners[listener] = nil
for _, atom in previousAtoms do
atom:unmount()
end
end
function listener(items)
local splitAtom = getSplitAtom()
if not splitAtom then
return cleanup()
end
local atoms = table.clone(splitAtom:get())
local itemsByKeys: { [Key]: Item } = {}
for key, item in items do
local id = if keyExtractor then keyExtractor(item) else key
itemsByKeys[id] = item
if atoms[id] then
-- Force set the atom to avoid redundantly updating itemsAtom
atoms[id]:setForced(item)
else
atoms[id] = atom(item):writable(function(update)
local nextItems = table.clone(itemsAtom:get())
nextItems[key] = update
itemsAtom:set(nextItems)
end)
end
end
for key, item in previousItems do
local id = if keyExtractor then keyExtractor(item) else key
local atom = atoms[id]
if itemsByKeys[id] == nil and atom then
atom:unmount()
atoms[id] = nil
end
end
previousAtoms, previousItems = atoms, items
splitAtom:setForced(atoms)
end
function splitAtom:unmount()
splitAtomUnmount(splitAtom)
cleanup()
end
itemsAtom.listeners[listener] = true
listener(itemsAtom:get())
return splitAtom
end
return (split :: any) :: Split

View file

@ -1,6 +1,5 @@
local atom = require(script.Parent.atom) local atom = require(script.Parent.atom)
type Atom<T> = atom.Atom<T> type Atom<T> = atom.Atom<T>
local reference = require(script.Parent.utils.reference) local reference = require(script.Parent.utils.reference)
local function subscribe<State>(atom: Atom<State>, callback: (state: State, previous: State) -> ()): (() -> (), () -> ()) local function subscribe<State>(atom: Atom<State>, callback: (state: State, previous: State) -> ()): (() -> (), () -> ())