From 22b440e35f10394069456332828e98ffee53221f Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Thu, 2 Apr 2026 06:49:54 -0700 Subject: [PATCH] refactor: use new type solver --- .github/workflows/ci.yml | 2 +- .luaurc | 18 ++-- .lute/check.luau | 1 + .lute/test.luau | 1 + README.md | 10 +- package.json | 2 +- packages/charm-sync/src/client.luau | 2 +- packages/charm-sync/src/patch.luau | 2 +- packages/charm-sync/src/server.luau | 25 +++-- packages/charm-sync/src/utils.luau | 10 +- packages/charm-sync/src/validate.luau | 5 +- packages/charm-sync/test/patch.test.luau | 2 +- packages/charm-sync/test/server.test.luau | 27 +++--- packages/charm-sync/test/utils.test.luau | 3 +- packages/charm/src/index.d.ts | 33 ++++--- packages/charm/src/init.luau | 92 ++++++++++--------- packages/charm/test/computed.test.luau | 6 +- packages/charm/test/effect.test.luau | 4 +- packages/charm/test/effectScope.test.luau | 6 +- packages/charm/test/gc.test.luau | 2 +- packages/charm/test/observe.test.luau | 8 +- packages/charm/test/signal.test.luau | 2 +- packages/charm/test/topology.test.luau | 16 ++-- packages/charm/test/untracked.test.luau | 2 +- packages/deep-charm/test/reactive.test.luau | 18 ++-- packages/react-charm/src/init.luau | 12 +-- .../react-charm/test/helpers/renderHook.luau | 3 +- packages/react-charm/test/init.luau | 1 + packages/vide-charm/src/init.luau | 2 +- .../vide-charm/test/useSignalState.test.luau | 4 +- selene.toml | 2 - tests/init.luau | 1 - 32 files changed, 169 insertions(+), 155 deletions(-) create mode 100644 .lute/test.luau delete mode 100644 tests/init.luau diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a02548c..912f2ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.luaurc b/.luaurc index c56c621..91ac9c9 100644 --- a/.luaurc +++ b/.luaurc @@ -1,10 +1,10 @@ { - "languageMode": "strict", - "aliases": { - "lute": "~/.lute/typedefs/0.1.0/lute", - "std": "~/.lute/typedefs/0.1.0/std" - }, - "lint": { - "LocalShadow": false - } -} \ No newline at end of file + "languageMode": "strict", + "aliases": { + "lute": "~/.lute/typedefs/0.1.0/lute", + "std": "~/.lute/typedefs/0.1.0/std" + }, + "lint": { + "LocalShadow": false + } +} diff --git a/.lute/check.luau b/.lute/check.luau index 4117286..ef48a74 100644 --- a/.lute/check.luau +++ b/.lute/check.luau @@ -16,6 +16,7 @@ local analyze = process.run({ "--ignore=**/node_modules/**", "--base-luaurc=.luaurc", "--definitions=roblox.d.luau", + "--flag:LuauSolverV2=true", "packages", "tests", "benches", diff --git a/.lute/test.luau b/.lute/test.luau new file mode 100644 index 0000000..4abbbb9 --- /dev/null +++ b/.lute/test.luau @@ -0,0 +1 @@ +(require)("../tests/run.server") diff --git a/README.md b/README.md index 71d5318..c337ff1 100644 --- a/README.md +++ b/README.md @@ -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" }) diff --git a/package.json b/package.json index 06671a3..c7774c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/charm-sync/src/client.luau b/packages/charm-sync/src/client.luau index 165bbc6..7b51359 100644 --- a/packages/charm-sync/src/client.luau +++ b/packages/charm-sync/src/client.luau @@ -3,7 +3,7 @@ local patch = require("./patch") local types = require("./types") type SyncPayload = types.SyncPayload -type ReactiveProxy = types.ReactiveProxy +type ReactiveProxy = types.ReactiveProxy local trackedSignals: { [string]: (any) -> any } = {} local trackedReactives: { [string]: ReactiveProxy } = {} diff --git a/packages/charm-sync/src/patch.luau b/packages/charm-sync/src/patch.luau index b9a3be1..d6b940f 100644 --- a/packages/charm-sync/src/patch.luau +++ b/packages/charm-sync/src/patch.luau @@ -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 diff --git a/packages/charm-sync/src/server.luau b/packages/charm-sync/src/server.luau index ce6fc9e..0fea67b 100644 --- a/packages/charm-sync/src/server.luau +++ b/packages/charm-sync/src/server.luau @@ -39,8 +39,9 @@ local syncCallback: ((client: any, payloads: { SyncPayload }) -> ())? support custom implementations of reactive proxies. ]] local function toRaw(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 diff --git a/packages/charm-sync/src/utils.luau b/packages/charm-sync/src/utils.luau index a4dae84..4a49871 100644 --- a/packages/charm-sync/src/utils.luau +++ b/packages/charm-sync/src/utils.luau @@ -2,10 +2,6 @@ local Charm = require("../Charm") type Atom = Charm.Atom -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(input: {}): Flattened +local function flatten(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 diff --git a/packages/charm-sync/src/validate.luau b/packages/charm-sync/src/validate.luau index ac21161..dcf5904 100644 --- a/packages/charm-sync/src/validate.luau +++ b/packages/charm-sync/src/validate.luau @@ -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` diff --git a/packages/charm-sync/test/patch.test.luau b/packages/charm-sync/test/patch.test.luau index 68d0483..076544e 100644 --- a/packages/charm-sync/test/patch.test.luau +++ b/packages/charm-sync/test/patch.test.luau @@ -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 diff --git a/packages/charm-sync/test/server.test.luau b/packages/charm-sync/test/server.test.luau index 02a9af3..380fbb0 100644 --- a/packages/charm-sync/test/server.test.luau +++ b/packages/charm-sync/test/server.test.luau @@ -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) diff --git a/packages/charm-sync/test/utils.test.luau b/packages/charm-sync/test/utils.test.luau index 14c9734..15dcabf 100644 --- a/packages/charm-sync/test/utils.test.luau +++ b/packages/charm-sync/test/utils.test.luau @@ -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 }>>(input) assert(result.a == input.a, "should flatten the first level") assert(result["b/c"] == input.b.c, "should flatten the second level") diff --git a/packages/charm/src/index.d.ts b/packages/charm/src/index.d.ts index 749f074..4e4ad8d 100644 --- a/packages/charm/src/index.d.ts +++ b/packages/charm/src/index.d.ts @@ -180,11 +180,11 @@ export function batch(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(getter: Getter, fn: (value: T, previousValue: T | undefined) => void): Cleanup; +export function listen(getter: Getter, 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(getter: Getter, 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(getter: Getter, fn: (value: T, previousValue: T) => void): Cleanup; +export function subscribe(getter: Getter, 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( getter: Getter | ReadonlyMap>, - mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO, + transform: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO, ): Getter>; // Overload for arrays export function mapped( getter: Getter, - mapper: (value: VI, key: number) => LuaTuple<[VO, K]> | VO, + transform: (value: VI, key: number) => LuaTuple<[VO, K]> | VO, ): Getter; // Overload for objects export function mapped( 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( * 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( getter: Getter | ReadonlyMap | readonly V[]>, - fn: (value: V, key: K) => Cleanup | void, + callback: (value: V, key: K) => Cleanup | void, ): Cleanup; // Overload for objects export function observe( getter: Getter<{ readonly [K in Key]: V }>, - fn: (value: V, key: K) => Cleanup | void, + callback: (value: V, key: K) => Cleanup | void, ): Cleanup; /** diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index b226dd2..f55c9ed 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -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(fn: (Args...) -> Results...): (Args...) -> Results... +local function noYield(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(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(initialValue: T, equals: Equals?): (Getter, Sette deepFreeze(initialValue) end - local function writeSignal(update: Update): T + local function writeSignal(newValue: Update): 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(initialValue: T, equals: Equals?): (Getter, 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(getter: (previousValue: T?) -> T): Getter } 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(getter: (previousValue: T?) -> T): Getter 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(getter: Getter, fn: (state: T, lastState: T?) -> ()): Cleanup - local tracked: Getter = if isSignal(getter) then getter else computed(getter) +local function listen(getter: Getter, callback: (state: T, lastState: T?) -> ()): Cleanup + local tracked: Getter = 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(getter: Getter, fn: (state: T, lastState: T) -> ()): Cleanup - local tracked: Getter = if isSignal(getter) then getter else computed(getter) +local function subscribe(getter: Getter, callback: (state: T, lastState: T) -> ()): Cleanup + local tracked: Getter = if isSignal(getter) then getter else computed(getter :: (T?) -> T) local current: T local firstRun = true @@ -844,7 +850,7 @@ local function subscribe(getter: Getter, 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(getter: Getter<{ [K]: V }>, fn: (item: V, key: K) -> ...Cleanup?): Cleanup +local function observe(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(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(getter: Getter<{ [KI]: VI }>, mapper: (VI, KI) -> (VO, KO)): Getter<{ [KO]: VO }> +local function mapped(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(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 :: (( getter: Getter<{ [KI]: VI }>, - mapper: (VI, KI) -> (VO, KO) - ) -> Getter<{ [KO]: VO }>) & ((getter: Getter<{ [K]: VI }>, mapper: (VI, K) -> VO) -> Getter<{ [K]: VO }>), + transform: (VI, KI) -> (VO, KO) + ) -> Getter<{ [KO]: VO }>) & ((getter: Getter<{ [K]: VI }>, transform: (VI, K) -> VO) -> Getter<{ [K]: VO }>), onCleanup = onCleanup, getActiveSub = getActiveSub, setActiveSub = setActiveSub, diff --git a/packages/charm/test/computed.test.luau b/packages/charm/test/computed.test.luau index 4590807..8985db2 100644 --- a/packages/charm/test/computed.test.luau +++ b/packages/charm/test/computed.test.luau @@ -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 {} diff --git a/packages/charm/test/effect.test.luau b/packages/charm/test/effect.test.luau index ef3c220..5eb4c94 100644 --- a/packages/charm/test/effect.test.luau +++ b/packages/charm/test/effect.test.luau @@ -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() diff --git a/packages/charm/test/effectScope.test.luau b/packages/charm/test/effectScope.test.luau index da05572..c1c06c2 100644 --- a/packages/charm/test/effectScope.test.luau +++ b/packages/charm/test/effectScope.test.luau @@ -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() diff --git a/packages/charm/test/gc.test.luau b/packages/charm/test/gc.test.luau index 2d53389..aed0af1 100644 --- a/packages/charm/test/gc.test.luau +++ b/packages/charm/test/gc.test.luau @@ -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)() diff --git a/packages/charm/test/observe.test.luau b/packages/charm/test/observe.test.luau index 86efdc3..c328f16 100644 --- a/packages/charm/test/observe.test.luau +++ b/packages/charm/test/observe.test.luau @@ -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}`) diff --git a/packages/charm/test/signal.test.luau b/packages/charm/test/signal.test.luau index ebc8e51..3d77f6c 100644 --- a/packages/charm/test/signal.test.luau +++ b/packages/charm/test/signal.test.luau @@ -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) diff --git a/packages/charm/test/topology.test.luau b/packages/charm/test/topology.test.luau index 06ee3c2..3675b3d 100644 --- a/packages/charm/test/topology.test.luau +++ b/packages/charm/test/topology.test.luau @@ -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") diff --git a/packages/charm/test/untracked.test.luau b/packages/charm/test/untracked.test.luau index cd75c63..3c7fea4 100644 --- a/packages/charm/test/untracked.test.luau +++ b/packages/charm/test/untracked.test.luau @@ -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() diff --git a/packages/deep-charm/test/reactive.test.luau b/packages/deep-charm/test/reactive.test.luau index 4b747c9..b663147 100644 --- a/packages/deep-charm/test/reactive.test.luau +++ b/packages/deep-charm/test/reactive.test.luau @@ -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 {} diff --git a/packages/react-charm/src/init.luau b/packages/react-charm/src/init.luau index 205514a..2d0cb9c 100644 --- a/packages/react-charm/src/init.luau +++ b/packages/react-charm/src/init.luau @@ -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(getter: () -> T, dependencies: { any }?): T +local function useSignalState(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(getter: () -> T, dependencies: { any }?) +local function useSignalBinding(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 diff --git a/packages/react-charm/test/helpers/renderHook.luau b/packages/react-charm/test/helpers/renderHook.luau index a059fb2..661d2e0 100644 --- a/packages/react-charm/test/helpers/renderHook.luau +++ b/packages/react-charm/test/helpers/renderHook.luau @@ -13,12 +13,13 @@ local function renderHook(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 diff --git a/packages/react-charm/test/init.luau b/packages/react-charm/test/init.luau index 25679e5..1871b99 100644 --- a/packages/react-charm/test/init.luau +++ b/packages/react-charm/test/init.luau @@ -1,5 +1,6 @@ local test = require("../../tests/test") +-- selene: allow(global_usage) _G.__ROACT_17_MOCK_SCHEDULER__ = true test.describe("useSignalState", function() diff --git a/packages/vide-charm/src/init.luau b/packages/vide-charm/src/init.luau index db9d2e7..5a27f54 100644 --- a/packages/vide-charm/src/init.luau +++ b/packages/vide-charm/src/init.luau @@ -11,7 +11,7 @@ local Vide = require("./Vide") local function useSignalState(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 diff --git a/packages/vide-charm/test/useSignalState.test.luau b/packages/vide-charm/test/useSignalState.test.luau index 3bfc74f..9661501 100644 --- a/packages/vide-charm/test/useSignalState.test.luau +++ b/packages/vide-charm/test/useSignalState.test.luau @@ -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() diff --git a/selene.toml b/selene.toml index 5a029a0..ecdec9c 100644 --- a/selene.toml +++ b/selene.toml @@ -2,6 +2,4 @@ std = "roblox" exclude = ["*/node_modules/*"] [lints] -global_usage = "allow" shadowing = "allow" -incorrect_standard_library_use = "allow" diff --git a/tests/init.luau b/tests/init.luau deleted file mode 100644 index 823aed6..0000000 --- a/tests/init.luau +++ /dev/null @@ -1 +0,0 @@ -(require)("@self/run.server")