mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: use new type solver
This commit is contained in:
parent
748dbc5014
commit
22b440e35f
32 changed files with 169 additions and 155 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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
16
.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
|
||||
}
|
||||
"languageMode": "strict",
|
||||
"aliases": {
|
||||
"lute": "~/.lute/typedefs/0.1.0/lute",
|
||||
"std": "~/.lute/typedefs/0.1.0/std"
|
||||
},
|
||||
"lint": {
|
||||
"LocalShadow": false
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
.lute/test.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
(require)("../tests/run.server")
|
||||
10
README.md
10
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" })
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 } = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
33
packages/charm/src/index.d.ts
vendored
33
packages/charm/src/index.d.ts
vendored
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)()
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
local test = require("../../tests/test")
|
||||
|
||||
-- selene: allow(global_usage)
|
||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||
|
||||
test.describe("useSignalState", function()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,4 @@ std = "roblox"
|
|||
exclude = ["*/node_modules/*"]
|
||||
|
||||
[lints]
|
||||
global_usage = "allow"
|
||||
shadowing = "allow"
|
||||
incorrect_standard_library_use = "allow"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
(require)("@self/run.server")
|
||||
Loading…
Add table
Add a link
Reference in a new issue