refactor: use new type solver

This commit is contained in:
littensy 2026-04-02 06:49:54 -07:00
parent 748dbc5014
commit 22b440e35f
32 changed files with 169 additions and 155 deletions

View file

@ -26,7 +26,7 @@ jobs:
pnpm prettier --check packages
- name: Run tests
run: lute run tests
run: lute run test
- name: Build models
run: lute run build

16
.luaurc
View file

@ -1,10 +1,10 @@
{
"languageMode": "strict",
"aliases": {
"lute": "~/.lute/typedefs/0.1.0/lute",
"std": "~/.lute/typedefs/0.1.0/std"
},
"lint": {
"LocalShadow": false
}
"languageMode": "strict",
"aliases": {
"lute": "~/.lute/typedefs/0.1.0/lute",
"std": "~/.lute/typedefs/0.1.0/std"
},
"lint": {
"LocalShadow": false
}
}

View file

@ -16,6 +16,7 @@ local analyze = process.run({
"--ignore=**/node_modules/**",
"--base-luaurc=.luaurc",
"--definitions=roblox.d.luau",
"--flag:LuauSolverV2=true",
"packages",
"tests",
"benches",

1
.lute/test.luau Normal file
View file

@ -0,0 +1 @@
(require)("../tests/run.server")

View file

@ -51,7 +51,7 @@ Charm is a state management library based on [fine-grained reactivity](https://d
- [`subscribe(getter, callback)`](#subscribegetter-callback)
- [`untracked(callback)`](#untrackedcallback)
- [`batch(callback)`](#batchcallback)
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
- [`mapped(getter, transform)`](#mappedgetter-transform)
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
- [`trigger(callback)`](#triggercallback)
@ -472,11 +472,11 @@ end)
---
### `mapped(getter, mapper)`
### `mapped(getter, transform)`
The `mapped` function iterates over every key in a table and uses the mapper to assign them to a new key and value. The result is returned as a read-only signal containing the new keys and values. When a key's value changes, or a new key is added to the table, the mapper is called for that key and its current value.
The `mapped` function iterates over every key in a table and uses the transform function to assign them to a new key and value. The result is returned as a read-only signal containing the new keys and values. When a key's value changes, or a new key is added to the table, `transform` is called for that key and its current value.
The first value returned by the mapper is used as the new value:
The first value returned by the transform function is used as the new value:
```luau
local getList, setList = signal({ "a", "b", "c" })
@ -488,7 +488,7 @@ end)
print(getUppercase()) -- { "A", "B", "C" }
```
If the mapper returns two values, the second value is used as the new key:
If the transform function returns two values, the second value is used as the new key:
```luau
local getList, setList = signal({ "a", "b", "c" })

View file

@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
"test": "lute run tests",
"test": "lute run test",
"build": "lute run build",
"check": "lute run check",
"publish:npm": "pnpm publish -r --access public",

View file

@ -3,7 +3,7 @@ local patch = require("./patch")
local types = require("./types")
type SyncPayload = types.SyncPayload
type ReactiveProxy<T = {}> = types.ReactiveProxy<T>
type ReactiveProxy<T = { [any]: any }> = types.ReactiveProxy<T>
local trackedSignals: { [string]: (any) -> any } = {}
local trackedReactives: { [string]: ReactiveProxy } = {}

View file

@ -39,7 +39,7 @@ end
long as the target array is not sparse. This function ensures that the diff
function produces a patch that can be correctly applied.
]]
local function stringifySparseArray(object: {}): { [string | number]: any }
local function stringifySparseArray(object: { [any]: any }): { [string | number]: any }
local maxn = table.maxn(object)
if maxn == 0 or maxn == #object then

View file

@ -39,8 +39,9 @@ local syncCallback: ((client: any, payloads: { SyncPayload }) -> ())?
support custom implementations of reactive proxies.
]]
local function toRaw<T>(value: T): T
local value: any = value
if type(value) == "table" and type(value.__target) == "table" and type(value.__track) == "function" then
value.__track()
(value.__track :: () -> ())()
return toRaw(value.__target)
end
return value
@ -153,15 +154,17 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
local status = clientKeys[key]
if status == LISTENING_FOR_CHANGES then
table.insert(payloads, {
local payload: SyncPayload = {
type = "patch",
data = { [key] = patchData },
})
}
table.insert(payloads, payload)
elseif status == PENDING_INITIAL_STATE then
table.insert(payloads, {
local payload: SyncPayload = {
type = "init",
data = { [key] = update.newValue },
})
}
table.insert(payloads, payload)
clientKeys[key] = LISTENING_FOR_CHANGES
end
end
@ -228,11 +231,19 @@ local function selectFromGlobalPatch(client: any, globalPatch: { [string]: any }
end
if next(initData) ~= nil then
table.insert(payloads, { type = "init", data = initData })
local payload: SyncPayload = {
type = "init",
data = initData,
}
table.insert(payloads, payload)
end
if next(patchData) ~= nil then
table.insert(payloads, { type = "patch", data = patchData })
local payload: SyncPayload = {
type = "patch",
data = patchData,
}
table.insert(payloads, payload)
end
return payloads

View file

@ -2,10 +2,6 @@ local Charm = require("../Charm")
type Atom<T> = Charm.Atom<T>
type NestedTable = {
[any]: NestedTable | any,
}
--[[
Flattens a nested table into a single table with slash-separated keys.
Useful for recursively collecting atoms returned by modules.
@ -13,15 +9,15 @@ type NestedTable = {
@param input A nested table.
@return A flattened table with slash-separated keys.
]]
local function flatten<Flattened>(input: {}): Flattened
local function flatten<Flattened>(input: { [any]: any }): Flattened
local result = {}
local function visit(node: {}, path: string)
local function visit(node: { [any]: any }, path: string)
for key, value in node do
local location = if path == "" then tostring(key) else path .. "/" .. tostring(key)
if type(value) == "table" then
visit(value :: {}, location)
visit(value, location)
else
result[location] = value
end

View file

@ -1,4 +1,4 @@
local function isTableUnsafe(object: {})
local function isTableUnsafe(object: { [any]: any })
local key = next(object)
if key == nil then
@ -31,9 +31,8 @@ end
with remote argument limitations, such as sparse arrays or mixed tables.
@param value The table value.
@param key The key of the value.
]]
local function validate(value: {})
local function validate(value: { [any]: any })
if isTableUnsafe(value) then
warn(
`Cannot sync tables unsupported by remote events! See the table below for more info.\n\n`

View file

@ -2,7 +2,7 @@ local config = require("../src/config")
local patch = require("../src/patch")
local test = require("../../../tests/test")
local function mockArraySerdes(object: {}): {}
local function mockArraySerdes(object: { [any]: any }): { [any]: any }
if object[1] then
local copy = {}
for index = 1, #object do

View file

@ -4,6 +4,7 @@ local config = require("../src/config")
local patch = require("../src/patch")
local server = require("../src/server")
local test = require("../../../tests/test")
local types = require("../src/types")
test.afterEach(function()
server.reset()
@ -14,7 +15,7 @@ test("should replicate state changes", function()
local a = Charm.atom(0)
local b = Charm.atom(0)
local c = Charm.atom(0)
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a, b = b, c = c })
server.connect(function(...)
@ -41,7 +42,7 @@ test("should replicate partial state patches", function()
local client = {}
local a = Charm.atom({ x = 0, y = 0 })
local b = Charm.atom(0 :: number?)
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a, b = b })
server.connect(function(...)
@ -68,7 +69,7 @@ end)
test("should replicate partial state patches (reactive)", function()
local client = {}
local a = DeepCharm.reactive({ x = { y = 0 }, z = 0 :: number? })
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(...)
@ -118,7 +119,7 @@ test("should init new clients", function()
local clientA = {}
local clientB = {}
local a = Charm.atom(0)
local payloadsA, payloadsB
local payloadsA: { types.SyncPayload }, payloadsB: { types.SyncPayload }
server.addSignalsToClient(clientA, { a = a })
server.connect(function(target, payloads)
@ -147,7 +148,7 @@ test("should init new atoms", function()
local client = {}
local a = Charm.atom(0)
local b = Charm.atom(0)
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(...)
@ -172,7 +173,7 @@ test("should only replicate to synced clients", function()
local clientB = {}
local a = Charm.atom(0)
local b = Charm.atom(0)
local payloadsA, payloadsB
local payloadsA: { types.SyncPayload }, payloadsB: { types.SyncPayload }
server.addSignalsToClient(clientA, { a = a })
server.addSignalsToClient(clientB, { b = b })
@ -214,7 +215,7 @@ end)
test("should serialize state", function()
local client = {}
local a = Charm.atom({ 1, 2, 3, 4, 5 })
local payloads
local payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(_, p)
@ -231,7 +232,7 @@ end)
test("should not send unchanged state", function()
local client = {}
local a = Charm.atom(0)
local payloads
local payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(_, p)
@ -262,7 +263,7 @@ test.describe("historical", function()
local a = Charm.atom(0)
local b = Charm.atom(0)
local c = Charm.atom(0)
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a, b = b, c = c })
server.connect(function(...)
@ -292,7 +293,7 @@ test.describe("historical", function()
local a = DeepCharm.reactive({ value = 0 })
local b = DeepCharm.reactive({ value = 0 })
local c = DeepCharm.reactive({ value = 0 })
local target, payloads
local target: unknown, payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a, b = b, c = c })
server.connect(function(...)
@ -321,7 +322,7 @@ test.describe("historical", function()
local clientA = {}
local clientB = {}
local a = Charm.atom(0)
local payloadsA, payloadsB
local payloadsA: { types.SyncPayload }, payloadsB: { types.SyncPayload }
server.addSignalsToClient(clientA, { a = a })
server.connect(function(target, payloads)
@ -357,7 +358,7 @@ test.describe("historical", function()
local client = {}
local a = Charm.atom(0)
local b = Charm.atom(0)
local payloads
local payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(_, ...)
@ -386,7 +387,7 @@ test.describe("historical", function()
test("should not send unchanged state", function()
local client = {}
local a = Charm.atom(0)
local payloads
local payloads: { types.SyncPayload }
server.addSignalsToClient(client, { a = a })
server.connect(function(_, p)

View file

@ -8,7 +8,8 @@ test("should flatten functions", function()
b = { c = Charm.atom(0) },
d = { e = { f = utils.signalToAtom(Charm.signal(0)) } },
}
local result = utils.flatten(input)
local result = utils.flatten<<{ [string]: Charm.Atom<number> }>>(input)
assert(result.a == input.a, "should flatten the first level")
assert(result["b/c"] == input.b.c, "should flatten the second level")

View file

@ -180,11 +180,11 @@ export function batch<Args extends any[], Result>(fn: (...args: Args) => Result,
* value and the previous value as arguments.
*
* @param getter A function that returns the value to watch.
* @param fn The function to run when the value changes.
* @param callback The function to run when the value changes.
* @returns A function for disposing the effect.
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
*/
export function listen<T>(getter: Getter<T>, fn: (value: T, previousValue: T | undefined) => void): Cleanup;
export function listen<T>(getter: Getter<T>, callback: (value: T, previousValue: T | undefined) => void): Cleanup;
/**
* Creates an effect that only runs the callback when the value returned by the
@ -192,34 +192,37 @@ export function listen<T>(getter: Getter<T>, fn: (value: T, previousValue: T | u
* as arguments.
*
* @param getter A function that returns the value to watch.
* @param fn The function to run when the value changes.
* @param callback The function to run when the value changes.
* @returns A function for disposing the effect.
* @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
*/
export function subscribe<T>(getter: Getter<T>, fn: (value: T, previousValue: T) => void): Cleanup;
export function subscribe<T>(getter: Getter<T>, callback: (value: T, previousValue: T) => void): Cleanup;
/**
* Creates a new read-only signal that is computed by mapping over the entries
* of an existing atom. If the key is omitted from the mapper's return value,
* the original key is preserved.
* of the source table. If the key is omitted from the transform function's
* return value, the original key is preserved.
*
* @param getter Returns the table to map over.
* @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
* This function is optimized to minimize unnecessary updates by only calling
* the transform function for entries that have changed since the last run.
*
* @param getter A signal or function that returns a table
* @param transform Function that maps each entry from the source to a new entry in the mapped table
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-transform
*/
export function mapped<VI, KI, VO, KO = KI>(
getter: Getter<Map<KI, VI> | ReadonlyMap<KI, VI>>,
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
transform: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
): Getter<ReadonlyMap<KO, VO>>;
// Overload for arrays
export function mapped<VI, VO, K extends Key = number>(
getter: Getter<readonly VI[]>,
mapper: (value: VI, key: number) => LuaTuple<[VO, K]> | VO,
transform: (value: VI, key: number) => LuaTuple<[VO, K]> | VO,
): Getter<K extends number ? readonly VO[] : { readonly [P in K]: VO }>;
// Overload for objects
export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
getter: Getter<{ readonly [K in KI]: VI }>,
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
transform: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
): Getter<{ readonly [K in KO]: VO }>;
/**
@ -234,18 +237,18 @@ export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
* from the table or when the callback is disposed.
*
* @param getter A function that returns the table to observe.
* @param fn A function that is called for each key in the table.
* @param callback A function that is called for each key in the table.
* @returns A function for disposing the observer.
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
*/
export function observe<V, K = number>(
getter: Getter<Map<K, V> | ReadonlyMap<K, V> | readonly V[]>,
fn: (value: V, key: K) => Cleanup | void,
callback: (value: V, key: K) => Cleanup | void,
): Cleanup;
// Overload for objects
export function observe<V, K extends Key>(
getter: Getter<{ readonly [K in Key]: V }>,
fn: (value: V, key: K) => Cleanup | void,
callback: (value: V, key: K) => Cleanup | void,
): Cleanup;
/**

View file

@ -96,12 +96,12 @@ local flags = {
Wraps a function to prevent yielding when in strict mode. If the function
attempts to yield, an error is thrown.
@param fn The function to wrap.
@param callback The function to wrap.
@return A wrapped function that cannot yield in strict mode.
]]
local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args...) -> Results...
local function noYield<Args..., Results...>(callback: (Args...) -> Results...): (Args...) -> Results...
if not flags.strict then
return fn
return callback
end
local function noYieldHandler(thread: thread, ok: boolean, ...)
@ -114,7 +114,7 @@ local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args.
end
return function(...)
local thread = coroutine.create(fn)
local thread = coroutine.create(callback)
return noYieldHandler(thread, coroutine.resume(thread, ...))
end
end
@ -195,7 +195,7 @@ local function runCleanups(cleanups: { Cleanup })
end
if hasErrors then
error(`Errors occurred during effect cleanup:\n\n${table.concat(errors, "\n\n")}`, 0)
error(`Errors occurred during effect cleanup:\n\n{table.concat(errors, "\n\n")}`, 0)
end
end
@ -206,9 +206,12 @@ end
@param effect Effect node to run
]]
local function run(effect: EffectNode)
local flags = effect.flags
local effectFlags = effect.flags
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
if
bit32.btest(effectFlags, DIRTY)
or (bit32.btest(effectFlags, PENDING) and checkDirty(effect.deps :: Link, effect))
then
runCleanups(effect.cleanups)
if effect.flags == NONE then
@ -342,10 +345,10 @@ end
inline the operation.
@param sub Effect or computed node
@param flags Subscriber's current flags
@param subFlags Subscriber's current flags
]]
local function clearPending(sub: ReactiveNode, flags: ReactiveFlags)
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
local function clearPending(sub: ReactiveNode, subFlags: ReactiveFlags)
sub.flags = bit32.band(subFlags, bit32.bnot(PENDING))
end
--[[
@ -463,14 +466,14 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
deepFreeze(initialValue)
end
local function writeSignal(update: Update<T>): T
local function writeSignal(newValue: Update<T>): T
local value: T
-- Determine the new value based on whether the update is a function or
-- a direct value
if type(update) == "function" then
if type(newValue) == "function" then
local prevSub = setActiveSub(nil)
local success, result = pcall(noYield(update), this.pendingValue)
local success, result = pcall(noYield(newValue), this.pendingValue)
activeSub = prevSub
if success then
@ -479,7 +482,7 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
error(result, 2)
end
else
value = update
value = newValue
end
-- Check if the new value is different from the pending value
@ -565,11 +568,14 @@ local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
}
local function computedOper(): T
local flags = this.flags
local computedFlags = this.flags
if
bit32.btest(flags, DIRTY)
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or clearPending(this, flags)))
bit32.btest(computedFlags, DIRTY)
or (
bit32.btest(computedFlags, PENDING)
and (checkDirty(this.deps :: Link, this) or clearPending(this, computedFlags))
)
then
if updateComputed(this) then
-- Notify subscribers if the value changed
@ -578,7 +584,7 @@ local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
shallowPropagate(subs)
end
end
elseif flags == NONE then
elseif computedFlags == NONE then
-- First access should initialize the computed value
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
@ -694,7 +700,7 @@ end
@param fn A signal or function that accesses signals
@see https://github.com/littensy/charm?tab=readme-ov-file#triggercallback
]]
local function trigger(fn: () -> ())
local function trigger(fn: () -> ...any)
local sub: ReactiveNode = {
flags = WATCHING,
}
@ -707,10 +713,10 @@ local function trigger(fn: () -> ())
error(result, 2)
end
local link = sub.deps
while link do
local dep = link.dep
link = unlink(link, sub)
local currentLink = sub.deps
while currentLink do
local dep = currentLink.dep
currentLink = unlink(currentLink, sub)
local subs = dep.subs
if subs then
sub.flags = NONE
@ -808,18 +814,18 @@ end
new value and the previous value as arguments.
@param getter A signal or function that accesses signals
@param fn Function to run when the value changes
@param callback Function to run when the value changes
@return A cleanup function that stops the effect
@see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
]]
local function listen<T>(getter: Getter<T>, fn: (state: T, lastState: T?) -> ()): Cleanup
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter)
local function listen<T>(getter: Getter<T>, callback: (state: T, lastState: T?) -> ()): Cleanup
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter :: (T?) -> T)
local current: T
return effect(function()
local previous = current
current = tracked()
untracked(fn, current, previous)
untracked(callback, current, previous)
end)
end
@ -829,12 +835,12 @@ end
value as arguments.
@param getter A signal or function that accesses signals
@param fn Function to run when the value changes
@param callback Function to run when the value changes
@return A cleanup function that stops the effect
@see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
]]
local function subscribe<T>(getter: Getter<T>, fn: (state: T, lastState: T) -> ()): Cleanup
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter)
local function subscribe<T>(getter: Getter<T>, callback: (state: T, lastState: T) -> ()): Cleanup
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter :: (T?) -> T)
local current: T
local firstRun = true
@ -844,7 +850,7 @@ local function subscribe<T>(getter: Getter<T>, fn: (state: T, lastState: T) -> (
if firstRun then
firstRun = false
else
untracked(fn, current, previous)
untracked(callback, current, previous)
end
end)
end
@ -859,11 +865,11 @@ end
during the callback will also be bound to the lifetime of the key.
@param getter A signal or function that returns a table
@param fn Function that is called for each key added to the table
@param callback Function that is called for each key added to the table
@return A cleanup function that disposes scopes for all keys
@see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
]]
local function observe<K, V>(getter: Getter<{ [K]: V }>, fn: (item: V, key: K) -> ...Cleanup?): Cleanup
local function observe<K, V>(getter: Getter<{ [K]: V }>, callback: (item: V, key: K) -> ...Cleanup?): Cleanup
local scopes: { [K]: Cleanup } = {}
local stopObserving = false
@ -881,7 +887,7 @@ local function observe<K, V>(getter: Getter<{ [K]: V }>, fn: (item: V, key: K) -
for key, value in data do
if scopes[key] == nil then
local dispose = effectScope(function()
return fn(value, key)
return callback(value, key)
end, true)
if stopObserving then
dispose()
@ -911,17 +917,17 @@ end
--[[
Creates a new read-only signal that is computed by mapping over the entries
of an existing atom. If the key is omitted from the mapper's return value,
the original key is preserved.
of the source table. If the key is omitted from the transform function's
return value, the original key is preserved.
This function is optimized to minimize unnecessary updates by only calling
the mapper for entries that have changed since the last run.
the transform function for entries that have changed since the last run.
@param getter A signal or function that returns a table
@param mapper Function that maps each entry from the source to a new entry in the mapped table
@see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
@param transform Function that maps each entry from the source to a new entry in the mapped table
@see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-transform
]]
local function mapped<KI, VI, KO, VO>(getter: Getter<{ [KI]: VI }>, mapper: (VI, KI) -> (VO, KO)): Getter<{ [KO]: VO }>
local function mapped<KI, VI, KO, VO>(getter: Getter<{ [KI]: VI }>, transform: (VI, KI) -> (VO, KO)): Getter<{ [KO]: VO }>
local nextInput: { [KI]: VI } = {}
local keyMap: { [KI]: KO } = {}
@ -957,7 +963,7 @@ local function mapped<KI, VI, KO, VO>(getter: Getter<{ [KI]: VI }>, mapper: (VI,
continue
end
local nextValueOut, keyOutOrNil = mapper(nextValueIn, keyIn)
local nextValueOut, keyOutOrNil = transform(nextValueIn, keyIn)
local nextKeyOut: KO = if keyOutOrNil ~= nil then keyOutOrNil else keyIn :: any
local prevKeyOut = keyMap[keyIn]
@ -999,8 +1005,8 @@ return {
observe = observe,
mapped = mapped :: (<KI, VI, KO, VO>(
getter: Getter<{ [KI]: VI }>,
mapper: (VI, KI) -> (VO, KO)
) -> Getter<{ [KO]: VO }>) & (<K, VI, VO>(getter: Getter<{ [K]: VI }>, mapper: (VI, K) -> VO) -> Getter<{ [K]: VO }>),
transform: (VI, KI) -> (VO, KO)
) -> Getter<{ [KO]: VO }>) & (<K, VI, VO>(getter: Getter<{ [K]: VI }>, transform: (VI, K) -> VO) -> Getter<{ [K]: VO }>),
onCleanup = onCleanup,
getActiveSub = getActiveSub,
setActiveSub = setActiveSub,

View file

@ -84,7 +84,7 @@ end)
test("should receive previous value", function()
local calls = {}
local src, setSrc = signal(0)
local c1 = computed(function(current): number
local c1 = computed(function(current: number?): number
local incoming = src()
return if current == incoming or incoming % 2 == 1 then current or incoming else incoming
end)
@ -192,10 +192,10 @@ test("should trigger effect even computed already dirty", function()
table.insert(calls, c2())
end)
assert(table.concat(calls, ",") == "0foo", table.concat(calls, ","))
assert(v() == 1, v())
assert(v() == 1, `expected 1 for v(), got {v()}`)
v(2)
assert(table.concat(calls, ",") == "0foo,2foo", table.concat(calls, ","))
assert(v() == 2, v())
assert(v() == 2, `expected 2 for v(), got {v()}`)
end)
return {}

View file

@ -440,7 +440,7 @@ end)
test("should run the cleanup if the effect disposes itself", function()
local a, setA = signal(0)
local calls = 0
local dispose
local dispose: () -> ()
dispose = effect(function()
if a() > 0 then
@ -460,7 +460,7 @@ end)
test("should not run the effect if the cleanup function disposes it", function()
local a, setA = signal(0)
local calls = 0
local dispose
local dispose: () -> ()
dispose = effect(function()
a()

View file

@ -31,8 +31,8 @@ end)
test("should run indirect scope cleanup", function()
local cleanups = 0
local cleanup: () -> ()
local cleanup
local stopScope = effectScope(function()
cleanup = effectScope(function()
onCleanup(function()
@ -50,8 +50,8 @@ end)
test("should run indirect effect cleanup", function()
local cleanups = 0
local a, setA = signal(false)
local stopScope = effectScope(function()
effect(function()
a()
@ -73,7 +73,7 @@ end)
test("should dispose inner effects if created in an effect", function()
local source, setSource = signal(1)
local triggers = 0
local dispose
local dispose: () -> ()
effect(function()
dispose = effectScope(function()

View file

@ -26,7 +26,7 @@ end
immediately closing it, causing alien-signals to unlink the computed value
from its dependencies and allow it to be garbage collected.
]]
local function unlink(getter: () -> ())
local function unlink(getter: () -> unknown)
effectScope(function()
getter()
end)()

View file

@ -95,7 +95,7 @@ test("should handle recursive additions and removals", function()
observe(items, function(item)
if item < 5 then
setItems(function(state)
setItems(function(state: { number })
state = table.clone(state)
table.insert(state, item + 1)
return state
@ -117,7 +117,7 @@ test("should handle self removal", function()
local edits = {}
observe(items, function(item, index)
setItems(function(state)
setItems(function(state: { number })
state = table.clone(state)
state[index] = nil
return state
@ -134,7 +134,7 @@ end)
test("should allow disposal during addition", function()
local items, setItems = signal({})
local edits = {}
local dispose
local dispose: () -> ()
dispose = observe(items, function(item)
if item == 1 then
@ -153,7 +153,7 @@ end)
test("should allow disposal during removal", function()
local items, setItems = signal({ 1, 2, 3, 4 })
local edits = {}
local dispose
local dispose: () -> ()
dispose = observe(items, function(item)
table.insert(edits, `+{item}`)

View file

@ -6,7 +6,7 @@ local effect = Charm.effect
test("should accept equality function", function()
local calls = {}
local src, setSrc = signal(0, function(current, incoming)
local src, setSrc = signal(0, function(current: number, incoming: number)
return current == incoming or incoming % 2 == 1
end)

View file

@ -442,18 +442,16 @@ end)
test.describe("error handling", function()
test("should keep graph consistent on errors during activation", function()
local a = atom(0)
local b = computed(function()
local b = computed(function(): nil
error("fail")
end)
local c = computed(function()
return a()
end)
local success, result = pcall(function()
b()
end)
local success, result = pcall(b)
assert(not success, "should throw")
assert(string.match(result, "fail"), "should throw fail")
assert(string.match(tostring(result), "fail"), "should throw fail")
a(1)
assert(c() == 1, "c should recover and return 1")
@ -461,7 +459,7 @@ test.describe("error handling", function()
test("should keep graph consistent on errors in computeds", function()
local a = atom(0)
local b = computed(function()
local b = computed(function(): number
if a() == 1 then
error("fail")
end
@ -474,11 +472,9 @@ test.describe("error handling", function()
assert(c() == 0, "c should be 0")
a(1)
local success, result = pcall(function()
b()
end)
local success, result = pcall(b)
assert(not success, "should throw")
assert(string.match(result, "fail"), "should throw fail")
assert(string.match(tostring(result), "fail"), "should throw fail")
a(2)
assert(c() == 2, "c should recover and return 2")

View file

@ -38,7 +38,7 @@ end)
test("should run detached from effect scope", function()
local effects = 0
local stopEffect
local stopEffect: () -> ()
local stopScope = effectScope(function()
untracked(function()

View file

@ -49,10 +49,10 @@ test("should unwrap proxy assignments", function()
end)
src.a, src.b = src.a, src.b
assert(runs == 1, runs)
assert(runs == 1, `bad runs: {runs}`)
src.a, src.b = src.b, src.a
assert(runs == 3, runs)
assert(runs == 3, `bad runs: {runs}`)
end)
test("should track iteration", function()
@ -125,10 +125,10 @@ test("should not track unread properties", function()
local _ = src.value.a
calls += 1
end)
assert(calls == 1, calls)
assert(calls == 1, `bad calls: {calls}`)
src.value.b = 3
assert(calls == 1, calls)
assert(calls == 1, `bad calls: {calls}`)
end)
test("should overwrite with table passed to setter", function()
@ -159,10 +159,10 @@ test("should unwrap proxy", function()
assert(toRaw(src) == raw, "unwrap did not return the original table")
assert(toRaw(raw) == raw, "unwrap modified a non-proxy value")
assert(runs == 1, runs)
assert(runs == 1, `bad runs: {runs}`)
src.x = 1
assert(runs == 2, runs)
assert(runs == 2, `bad runs: {runs}`)
end)
test("should unwrap nested proxy", function()
@ -177,13 +177,13 @@ test("should unwrap nested proxy", function()
end)
assert(toRaw(src) == raw, "unwrap did not return the original table")
assert(runs == 1, runs)
assert(runs == 1, `bad runs: {runs}`)
inner.x = 1
assert(runs == 2, runs)
assert(runs == 2, `bad runs: {runs}`)
src.y = 2
assert(runs == 3, runs)
assert(runs == 3, `bad runs: {runs}`)
end)
return {}

View file

@ -13,12 +13,12 @@ local React = require("./React")
@param dependencies An array of values used to memoize the getter.
@return The current state.
]]
local function useSignalState<T>(getter: () -> T, dependencies: { any }?): T
local function useSignalState<T>(getter: () -> T, dependencies: {}?): T
local value, setValue = React.useState(getter)
React.useLayoutEffect(function()
return Charm.listen(getter, setValue)
end, dependencies or {})
return Charm.listen(getter, setValue :: (T, T?) -> ())
end, dependencies :: { any } or {})
return value
end
@ -34,13 +34,13 @@ end
@param dependencies An array of values used to memoize the getter.
@return A binding that stores the current state.
]]
local function useSignalBinding<T>(getter: () -> T, dependencies: { any }?)
local function useSignalBinding<T>(getter: () -> T, dependencies: {}?)
local initialValue = React.useMemo(getter, {})
local binding, setBinding = React.useBinding(initialValue)
React.useLayoutEffect(function()
return Charm.listen(getter, setBinding)
end, dependencies or {})
return Charm.listen(getter, setBinding :: (T, T?) -> ())
end, dependencies :: { any } or {})
return binding
end

View file

@ -13,12 +13,13 @@ local function renderHook<Props, Result>(callback: (Props) -> Result, props: Pro
local function Component(props: Props)
hook.result = callback(props)
return nil
end
function hook.rerender(newProps: Props?)
props = newProps or props
ReactRoblox.act(function()
root:render(React.createElement(Component, props))
root:render(React.createElement(Component, props) :: any)
end)
end

View file

@ -1,5 +1,6 @@
local test = require("../../tests/test")
-- selene: allow(global_usage)
_G.__ROACT_17_MOCK_SCHEDULER__ = true
test.describe("useSignalState", function()

View file

@ -11,7 +11,7 @@ local Vide = require("./Vide")
local function useSignalState<State>(getter: () -> State): () -> State
local state = Vide.source(getter())
Vide.cleanup(Charm.listen(getter, state))
Vide.cleanup(Charm.listen(getter, state :: (State, State?) -> ()))
return state
end

View file

@ -5,7 +5,7 @@ local test = require("../../../tests/test")
test("tracks signal state", function()
local getState, setState = Charm.signal(0)
local source
local source: () -> number
local cleanup = Vide.root(function()
source = VideCharm.useSignalState(getState)
end)
@ -17,7 +17,7 @@ end)
test("accepts a selector", function()
local getState, setState = Charm.signal(0)
local source
local source: () -> number
local cleanup = Vide.root(function()
source = VideCharm.useSignalState(function()
return 2 * getState()

View file

@ -2,6 +2,4 @@ std = "roblox"
exclude = ["*/node_modules/*"]
[lints]
global_usage = "allow"
shadowing = "allow"
incorrect_standard_library_use = "allow"

View file

@ -1 +0,0 @@
(require)("@self/run.server")