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
|
pnpm prettier --check packages
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: lute run tests
|
run: lute run test
|
||||||
|
|
||||||
- name: Build models
|
- name: Build models
|
||||||
run: lute run build
|
run: lute run build
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ local analyze = process.run({
|
||||||
"--ignore=**/node_modules/**",
|
"--ignore=**/node_modules/**",
|
||||||
"--base-luaurc=.luaurc",
|
"--base-luaurc=.luaurc",
|
||||||
"--definitions=roblox.d.luau",
|
"--definitions=roblox.d.luau",
|
||||||
|
"--flag:LuauSolverV2=true",
|
||||||
"packages",
|
"packages",
|
||||||
"tests",
|
"tests",
|
||||||
"benches",
|
"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)
|
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
||||||
- [`untracked(callback)`](#untrackedcallback)
|
- [`untracked(callback)`](#untrackedcallback)
|
||||||
- [`batch(callback)`](#batchcallback)
|
- [`batch(callback)`](#batchcallback)
|
||||||
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
- [`mapped(getter, transform)`](#mappedgetter-transform)
|
||||||
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
||||||
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
||||||
- [`trigger(callback)`](#triggercallback)
|
- [`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
|
```luau
|
||||||
local getList, setList = signal({ "a", "b", "c" })
|
local getList, setList = signal({ "a", "b", "c" })
|
||||||
|
|
@ -488,7 +488,7 @@ end)
|
||||||
print(getUppercase()) -- { "A", "B", "C" }
|
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
|
```luau
|
||||||
local getList, setList = signal({ "a", "b", "c" })
|
local getList, setList = signal({ "a", "b", "c" })
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "lute run tests",
|
"test": "lute run test",
|
||||||
"build": "lute run build",
|
"build": "lute run build",
|
||||||
"check": "lute run check",
|
"check": "lute run check",
|
||||||
"publish:npm": "pnpm publish -r --access public",
|
"publish:npm": "pnpm publish -r --access public",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ local patch = require("./patch")
|
||||||
local types = require("./types")
|
local types = require("./types")
|
||||||
|
|
||||||
type SyncPayload = types.SyncPayload
|
type SyncPayload = types.SyncPayload
|
||||||
type ReactiveProxy<T = {}> = types.ReactiveProxy<T>
|
type ReactiveProxy<T = { [any]: any }> = types.ReactiveProxy<T>
|
||||||
|
|
||||||
local trackedSignals: { [string]: (any) -> any } = {}
|
local trackedSignals: { [string]: (any) -> any } = {}
|
||||||
local trackedReactives: { [string]: ReactiveProxy } = {}
|
local trackedReactives: { [string]: ReactiveProxy } = {}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ end
|
||||||
long as the target array is not sparse. This function ensures that the diff
|
long as the target array is not sparse. This function ensures that the diff
|
||||||
function produces a patch that can be correctly applied.
|
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)
|
local maxn = table.maxn(object)
|
||||||
|
|
||||||
if maxn == 0 or maxn == #object then
|
if maxn == 0 or maxn == #object then
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ local syncCallback: ((client: any, payloads: { SyncPayload }) -> ())?
|
||||||
support custom implementations of reactive proxies.
|
support custom implementations of reactive proxies.
|
||||||
]]
|
]]
|
||||||
local function toRaw<T>(value: T): T
|
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
|
if type(value) == "table" and type(value.__target) == "table" and type(value.__track) == "function" then
|
||||||
value.__track()
|
(value.__track :: () -> ())()
|
||||||
return toRaw(value.__target)
|
return toRaw(value.__target)
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
|
|
@ -153,15 +154,17 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
|
||||||
local status = clientKeys[key]
|
local status = clientKeys[key]
|
||||||
|
|
||||||
if status == LISTENING_FOR_CHANGES then
|
if status == LISTENING_FOR_CHANGES then
|
||||||
table.insert(payloads, {
|
local payload: SyncPayload = {
|
||||||
type = "patch",
|
type = "patch",
|
||||||
data = { [key] = patchData },
|
data = { [key] = patchData },
|
||||||
})
|
}
|
||||||
|
table.insert(payloads, payload)
|
||||||
elseif status == PENDING_INITIAL_STATE then
|
elseif status == PENDING_INITIAL_STATE then
|
||||||
table.insert(payloads, {
|
local payload: SyncPayload = {
|
||||||
type = "init",
|
type = "init",
|
||||||
data = { [key] = update.newValue },
|
data = { [key] = update.newValue },
|
||||||
})
|
}
|
||||||
|
table.insert(payloads, payload)
|
||||||
clientKeys[key] = LISTENING_FOR_CHANGES
|
clientKeys[key] = LISTENING_FOR_CHANGES
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -228,11 +231,19 @@ local function selectFromGlobalPatch(client: any, globalPatch: { [string]: any }
|
||||||
end
|
end
|
||||||
|
|
||||||
if next(initData) ~= nil then
|
if next(initData) ~= nil then
|
||||||
table.insert(payloads, { type = "init", data = initData })
|
local payload: SyncPayload = {
|
||||||
|
type = "init",
|
||||||
|
data = initData,
|
||||||
|
}
|
||||||
|
table.insert(payloads, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
if next(patchData) ~= nil then
|
if next(patchData) ~= nil then
|
||||||
table.insert(payloads, { type = "patch", data = patchData })
|
local payload: SyncPayload = {
|
||||||
|
type = "patch",
|
||||||
|
data = patchData,
|
||||||
|
}
|
||||||
|
table.insert(payloads, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
return payloads
|
return payloads
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@ local Charm = require("../Charm")
|
||||||
|
|
||||||
type Atom<T> = Charm.Atom<T>
|
type Atom<T> = Charm.Atom<T>
|
||||||
|
|
||||||
type NestedTable = {
|
|
||||||
[any]: NestedTable | any,
|
|
||||||
}
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Flattens a nested table into a single table with slash-separated keys.
|
Flattens a nested table into a single table with slash-separated keys.
|
||||||
Useful for recursively collecting atoms returned by modules.
|
Useful for recursively collecting atoms returned by modules.
|
||||||
|
|
@ -13,15 +9,15 @@ type NestedTable = {
|
||||||
@param input A nested table.
|
@param input A nested table.
|
||||||
@return A flattened table with slash-separated keys.
|
@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 result = {}
|
||||||
|
|
||||||
local function visit(node: {}, path: string)
|
local function visit(node: { [any]: any }, path: string)
|
||||||
for key, value in node do
|
for key, value in node do
|
||||||
local location = if path == "" then tostring(key) else path .. "/" .. tostring(key)
|
local location = if path == "" then tostring(key) else path .. "/" .. tostring(key)
|
||||||
|
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
visit(value :: {}, location)
|
visit(value, location)
|
||||||
else
|
else
|
||||||
result[location] = value
|
result[location] = value
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
local function isTableUnsafe(object: {})
|
local function isTableUnsafe(object: { [any]: any })
|
||||||
local key = next(object)
|
local key = next(object)
|
||||||
|
|
||||||
if key == nil then
|
if key == nil then
|
||||||
|
|
@ -31,9 +31,8 @@ end
|
||||||
with remote argument limitations, such as sparse arrays or mixed tables.
|
with remote argument limitations, such as sparse arrays or mixed tables.
|
||||||
|
|
||||||
@param value The table value.
|
@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
|
if isTableUnsafe(value) then
|
||||||
warn(
|
warn(
|
||||||
`Cannot sync tables unsupported by remote events! See the table below for more info.\n\n`
|
`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 patch = require("../src/patch")
|
||||||
local test = require("../../../tests/test")
|
local test = require("../../../tests/test")
|
||||||
|
|
||||||
local function mockArraySerdes(object: {}): {}
|
local function mockArraySerdes(object: { [any]: any }): { [any]: any }
|
||||||
if object[1] then
|
if object[1] then
|
||||||
local copy = {}
|
local copy = {}
|
||||||
for index = 1, #object do
|
for index = 1, #object do
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ local config = require("../src/config")
|
||||||
local patch = require("../src/patch")
|
local patch = require("../src/patch")
|
||||||
local server = require("../src/server")
|
local server = require("../src/server")
|
||||||
local test = require("../../../tests/test")
|
local test = require("../../../tests/test")
|
||||||
|
local types = require("../src/types")
|
||||||
|
|
||||||
test.afterEach(function()
|
test.afterEach(function()
|
||||||
server.reset()
|
server.reset()
|
||||||
|
|
@ -14,7 +15,7 @@ test("should replicate state changes", function()
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local b = Charm.atom(0)
|
local b = Charm.atom(0)
|
||||||
local c = 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.addSignalsToClient(client, { a = a, b = b, c = c })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -41,7 +42,7 @@ test("should replicate partial state patches", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom({ x = 0, y = 0 })
|
local a = Charm.atom({ x = 0, y = 0 })
|
||||||
local b = Charm.atom(0 :: number?)
|
local b = Charm.atom(0 :: number?)
|
||||||
local target, payloads
|
local target: unknown, payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a, b = b })
|
server.addSignalsToClient(client, { a = a, b = b })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -68,7 +69,7 @@ end)
|
||||||
test("should replicate partial state patches (reactive)", function()
|
test("should replicate partial state patches (reactive)", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = DeepCharm.reactive({ x = { y = 0 }, z = 0 :: number? })
|
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.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -118,7 +119,7 @@ test("should init new clients", function()
|
||||||
local clientA = {}
|
local clientA = {}
|
||||||
local clientB = {}
|
local clientB = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local payloadsA, payloadsB
|
local payloadsA: { types.SyncPayload }, payloadsB: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(clientA, { a = a })
|
server.addSignalsToClient(clientA, { a = a })
|
||||||
server.connect(function(target, payloads)
|
server.connect(function(target, payloads)
|
||||||
|
|
@ -147,7 +148,7 @@ test("should init new atoms", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local b = Charm.atom(0)
|
local b = Charm.atom(0)
|
||||||
local target, payloads
|
local target: unknown, payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a })
|
server.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -172,7 +173,7 @@ test("should only replicate to synced clients", function()
|
||||||
local clientB = {}
|
local clientB = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local b = 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(clientA, { a = a })
|
||||||
server.addSignalsToClient(clientB, { b = b })
|
server.addSignalsToClient(clientB, { b = b })
|
||||||
|
|
@ -214,7 +215,7 @@ end)
|
||||||
test("should serialize state", function()
|
test("should serialize state", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom({ 1, 2, 3, 4, 5 })
|
local a = Charm.atom({ 1, 2, 3, 4, 5 })
|
||||||
local payloads
|
local payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a })
|
server.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(_, p)
|
server.connect(function(_, p)
|
||||||
|
|
@ -231,7 +232,7 @@ end)
|
||||||
test("should not send unchanged state", function()
|
test("should not send unchanged state", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local payloads
|
local payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a })
|
server.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(_, p)
|
server.connect(function(_, p)
|
||||||
|
|
@ -262,7 +263,7 @@ test.describe("historical", function()
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local b = Charm.atom(0)
|
local b = Charm.atom(0)
|
||||||
local c = 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.addSignalsToClient(client, { a = a, b = b, c = c })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -292,7 +293,7 @@ test.describe("historical", function()
|
||||||
local a = DeepCharm.reactive({ value = 0 })
|
local a = DeepCharm.reactive({ value = 0 })
|
||||||
local b = DeepCharm.reactive({ value = 0 })
|
local b = DeepCharm.reactive({ value = 0 })
|
||||||
local c = 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.addSignalsToClient(client, { a = a, b = b, c = c })
|
||||||
server.connect(function(...)
|
server.connect(function(...)
|
||||||
|
|
@ -321,7 +322,7 @@ test.describe("historical", function()
|
||||||
local clientA = {}
|
local clientA = {}
|
||||||
local clientB = {}
|
local clientB = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local payloadsA, payloadsB
|
local payloadsA: { types.SyncPayload }, payloadsB: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(clientA, { a = a })
|
server.addSignalsToClient(clientA, { a = a })
|
||||||
server.connect(function(target, payloads)
|
server.connect(function(target, payloads)
|
||||||
|
|
@ -357,7 +358,7 @@ test.describe("historical", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local b = Charm.atom(0)
|
local b = Charm.atom(0)
|
||||||
local payloads
|
local payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a })
|
server.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(_, ...)
|
server.connect(function(_, ...)
|
||||||
|
|
@ -386,7 +387,7 @@ test.describe("historical", function()
|
||||||
test("should not send unchanged state", function()
|
test("should not send unchanged state", function()
|
||||||
local client = {}
|
local client = {}
|
||||||
local a = Charm.atom(0)
|
local a = Charm.atom(0)
|
||||||
local payloads
|
local payloads: { types.SyncPayload }
|
||||||
|
|
||||||
server.addSignalsToClient(client, { a = a })
|
server.addSignalsToClient(client, { a = a })
|
||||||
server.connect(function(_, p)
|
server.connect(function(_, p)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ test("should flatten functions", function()
|
||||||
b = { c = Charm.atom(0) },
|
b = { c = Charm.atom(0) },
|
||||||
d = { e = { f = utils.signalToAtom(Charm.signal(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.a == input.a, "should flatten the first level")
|
||||||
assert(result["b/c"] == input.b.c, "should flatten the second 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.
|
* value and the previous value as arguments.
|
||||||
*
|
*
|
||||||
* @param getter A function that returns the value to watch.
|
* @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.
|
* @returns A function for disposing the effect.
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
* @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
|
* 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.
|
* as arguments.
|
||||||
*
|
*
|
||||||
* @param getter A function that returns the value to watch.
|
* @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.
|
* @returns A function for disposing the effect.
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
* @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
|
* 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,
|
* of the source table. If the key is omitted from the transform function's
|
||||||
* the original key is preserved.
|
* return value, the original key is preserved.
|
||||||
*
|
*
|
||||||
* @param getter Returns the table to map over.
|
* This function is optimized to minimize unnecessary updates by only calling
|
||||||
* @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
* the transform function for entries that have changed since the last run.
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
*
|
||||||
|
* @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>(
|
export function mapped<VI, KI, VO, KO = KI>(
|
||||||
getter: Getter<Map<KI, VI> | ReadonlyMap<KI, VI>>,
|
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>>;
|
): Getter<ReadonlyMap<KO, VO>>;
|
||||||
// Overload for arrays
|
// Overload for arrays
|
||||||
export function mapped<VI, VO, K extends Key = number>(
|
export function mapped<VI, VO, K extends Key = number>(
|
||||||
getter: Getter<readonly VI[]>,
|
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 }>;
|
): Getter<K extends number ? readonly VO[] : { readonly [P in K]: VO }>;
|
||||||
// Overload for objects
|
// Overload for objects
|
||||||
export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
|
export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
|
||||||
getter: Getter<{ readonly [K in KI]: VI }>,
|
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 }>;
|
): 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.
|
* from the table or when the callback is disposed.
|
||||||
*
|
*
|
||||||
* @param getter A function that returns the table to observe.
|
* @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.
|
* @returns A function for disposing the observer.
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||||
*/
|
*/
|
||||||
export function observe<V, K = number>(
|
export function observe<V, K = number>(
|
||||||
getter: Getter<Map<K, V> | ReadonlyMap<K, V> | readonly V[]>,
|
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;
|
): Cleanup;
|
||||||
// Overload for objects
|
// Overload for objects
|
||||||
export function observe<V, K extends Key>(
|
export function observe<V, K extends Key>(
|
||||||
getter: Getter<{ readonly [K in Key]: V }>,
|
getter: Getter<{ readonly [K in Key]: V }>,
|
||||||
fn: (value: V, key: K) => Cleanup | void,
|
callback: (value: V, key: K) => Cleanup | void,
|
||||||
): Cleanup;
|
): Cleanup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -96,12 +96,12 @@ local flags = {
|
||||||
Wraps a function to prevent yielding when in strict mode. If the function
|
Wraps a function to prevent yielding when in strict mode. If the function
|
||||||
attempts to yield, an error is thrown.
|
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.
|
@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
|
if not flags.strict then
|
||||||
return fn
|
return callback
|
||||||
end
|
end
|
||||||
|
|
||||||
local function noYieldHandler(thread: thread, ok: boolean, ...)
|
local function noYieldHandler(thread: thread, ok: boolean, ...)
|
||||||
|
|
@ -114,7 +114,7 @@ local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args.
|
||||||
end
|
end
|
||||||
|
|
||||||
return function(...)
|
return function(...)
|
||||||
local thread = coroutine.create(fn)
|
local thread = coroutine.create(callback)
|
||||||
return noYieldHandler(thread, coroutine.resume(thread, ...))
|
return noYieldHandler(thread, coroutine.resume(thread, ...))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -195,7 +195,7 @@ local function runCleanups(cleanups: { Cleanup })
|
||||||
end
|
end
|
||||||
|
|
||||||
if hasErrors then
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -206,9 +206,12 @@ end
|
||||||
@param effect Effect node to run
|
@param effect Effect node to run
|
||||||
]]
|
]]
|
||||||
local function run(effect: EffectNode)
|
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)
|
runCleanups(effect.cleanups)
|
||||||
|
|
||||||
if effect.flags == NONE then
|
if effect.flags == NONE then
|
||||||
|
|
@ -342,10 +345,10 @@ end
|
||||||
inline the operation.
|
inline the operation.
|
||||||
|
|
||||||
@param sub Effect or computed node
|
@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)
|
local function clearPending(sub: ReactiveNode, subFlags: ReactiveFlags)
|
||||||
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
sub.flags = bit32.band(subFlags, bit32.bnot(PENDING))
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
@ -463,14 +466,14 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
deepFreeze(initialValue)
|
deepFreeze(initialValue)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function writeSignal(update: Update<T>): T
|
local function writeSignal(newValue: Update<T>): T
|
||||||
local value: T
|
local value: T
|
||||||
|
|
||||||
-- Determine the new value based on whether the update is a function or
|
-- Determine the new value based on whether the update is a function or
|
||||||
-- a direct value
|
-- a direct value
|
||||||
if type(update) == "function" then
|
if type(newValue) == "function" then
|
||||||
local prevSub = setActiveSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, result = pcall(noYield(update), this.pendingValue)
|
local success, result = pcall(noYield(newValue), this.pendingValue)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
|
|
@ -479,7 +482,7 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
error(result, 2)
|
error(result, 2)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
value = update
|
value = newValue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if the new value is different from the pending value
|
-- 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 function computedOper(): T
|
||||||
local flags = this.flags
|
local computedFlags = this.flags
|
||||||
|
|
||||||
if
|
if
|
||||||
bit32.btest(flags, DIRTY)
|
bit32.btest(computedFlags, DIRTY)
|
||||||
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or clearPending(this, flags)))
|
or (
|
||||||
|
bit32.btest(computedFlags, PENDING)
|
||||||
|
and (checkDirty(this.deps :: Link, this) or clearPending(this, computedFlags))
|
||||||
|
)
|
||||||
then
|
then
|
||||||
if updateComputed(this) then
|
if updateComputed(this) then
|
||||||
-- Notify subscribers if the value changed
|
-- Notify subscribers if the value changed
|
||||||
|
|
@ -578,7 +584,7 @@ local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif flags == NONE then
|
elseif computedFlags == NONE then
|
||||||
-- First access should initialize the computed value
|
-- First access should initialize the computed value
|
||||||
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||||
|
|
||||||
|
|
@ -694,7 +700,7 @@ end
|
||||||
@param fn A signal or function that accesses signals
|
@param fn A signal or function that accesses signals
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#triggercallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#triggercallback
|
||||||
]]
|
]]
|
||||||
local function trigger(fn: () -> ())
|
local function trigger(fn: () -> ...any)
|
||||||
local sub: ReactiveNode = {
|
local sub: ReactiveNode = {
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
}
|
}
|
||||||
|
|
@ -707,10 +713,10 @@ local function trigger(fn: () -> ())
|
||||||
error(result, 2)
|
error(result, 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local link = sub.deps
|
local currentLink = sub.deps
|
||||||
while link do
|
while currentLink do
|
||||||
local dep = link.dep
|
local dep = currentLink.dep
|
||||||
link = unlink(link, sub)
|
currentLink = unlink(currentLink, sub)
|
||||||
local subs = dep.subs
|
local subs = dep.subs
|
||||||
if subs then
|
if subs then
|
||||||
sub.flags = NONE
|
sub.flags = NONE
|
||||||
|
|
@ -808,18 +814,18 @@ end
|
||||||
new value and the previous value as arguments.
|
new value and the previous value as arguments.
|
||||||
|
|
||||||
@param getter A signal or function that accesses signals
|
@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
|
@return A cleanup function that stops the effect
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
@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 function listen<T>(getter: Getter<T>, callback: (state: T, lastState: T?) -> ()): Cleanup
|
||||||
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter)
|
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter :: (T?) -> T)
|
||||||
local current: T
|
local current: T
|
||||||
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
untracked(fn, current, previous)
|
untracked(callback, current, previous)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -829,12 +835,12 @@ end
|
||||||
value as arguments.
|
value as arguments.
|
||||||
|
|
||||||
@param getter A signal or function that accesses signals
|
@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
|
@return A cleanup function that stops the effect
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
@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 function subscribe<T>(getter: Getter<T>, callback: (state: T, lastState: T) -> ()): Cleanup
|
||||||
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter)
|
local tracked: Getter<T> = if isSignal(getter) then getter else computed(getter :: (T?) -> T)
|
||||||
local current: T
|
local current: T
|
||||||
local firstRun = true
|
local firstRun = true
|
||||||
|
|
||||||
|
|
@ -844,7 +850,7 @@ local function subscribe<T>(getter: Getter<T>, fn: (state: T, lastState: T) -> (
|
||||||
if firstRun then
|
if firstRun then
|
||||||
firstRun = false
|
firstRun = false
|
||||||
else
|
else
|
||||||
untracked(fn, current, previous)
|
untracked(callback, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -859,11 +865,11 @@ end
|
||||||
during the callback will also be bound to the lifetime of the key.
|
during the callback will also be bound to the lifetime of the key.
|
||||||
|
|
||||||
@param getter A signal or function that returns a table
|
@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
|
@return A cleanup function that disposes scopes for all keys
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
@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 scopes: { [K]: Cleanup } = {}
|
||||||
local stopObserving = false
|
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
|
for key, value in data do
|
||||||
if scopes[key] == nil then
|
if scopes[key] == nil then
|
||||||
local dispose = effectScope(function()
|
local dispose = effectScope(function()
|
||||||
return fn(value, key)
|
return callback(value, key)
|
||||||
end, true)
|
end, true)
|
||||||
if stopObserving then
|
if stopObserving then
|
||||||
dispose()
|
dispose()
|
||||||
|
|
@ -911,17 +917,17 @@ end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Creates a new read-only signal that is computed by mapping over the entries
|
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,
|
of the source table. If the key is omitted from the transform function's
|
||||||
the original key is preserved.
|
return value, the original key is preserved.
|
||||||
|
|
||||||
This function is optimized to minimize unnecessary updates by only calling
|
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 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
|
@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-mapper
|
@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 nextInput: { [KI]: VI } = {}
|
||||||
local keyMap: { [KI]: KO } = {}
|
local keyMap: { [KI]: KO } = {}
|
||||||
|
|
||||||
|
|
@ -957,7 +963,7 @@ local function mapped<KI, VI, KO, VO>(getter: Getter<{ [KI]: VI }>, mapper: (VI,
|
||||||
continue
|
continue
|
||||||
end
|
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 nextKeyOut: KO = if keyOutOrNil ~= nil then keyOutOrNil else keyIn :: any
|
||||||
local prevKeyOut = keyMap[keyIn]
|
local prevKeyOut = keyMap[keyIn]
|
||||||
|
|
||||||
|
|
@ -999,8 +1005,8 @@ return {
|
||||||
observe = observe,
|
observe = observe,
|
||||||
mapped = mapped :: (<KI, VI, KO, VO>(
|
mapped = mapped :: (<KI, VI, KO, VO>(
|
||||||
getter: Getter<{ [KI]: VI }>,
|
getter: Getter<{ [KI]: VI }>,
|
||||||
mapper: (VI, KI) -> (VO, KO)
|
transform: (VI, KI) -> (VO, KO)
|
||||||
) -> Getter<{ [KO]: VO }>) & (<K, VI, VO>(getter: Getter<{ [K]: VI }>, mapper: (VI, K) -> VO) -> Getter<{ [K]: VO }>),
|
) -> Getter<{ [KO]: VO }>) & (<K, VI, VO>(getter: Getter<{ [K]: VI }>, transform: (VI, K) -> VO) -> Getter<{ [K]: VO }>),
|
||||||
onCleanup = onCleanup,
|
onCleanup = onCleanup,
|
||||||
getActiveSub = getActiveSub,
|
getActiveSub = getActiveSub,
|
||||||
setActiveSub = setActiveSub,
|
setActiveSub = setActiveSub,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ end)
|
||||||
test("should receive previous value", function()
|
test("should receive previous value", function()
|
||||||
local calls = {}
|
local calls = {}
|
||||||
local src, setSrc = signal(0)
|
local src, setSrc = signal(0)
|
||||||
local c1 = computed(function(current): number
|
local c1 = computed(function(current: number?): number
|
||||||
local incoming = src()
|
local incoming = src()
|
||||||
return if current == incoming or incoming % 2 == 1 then current or incoming else incoming
|
return if current == incoming or incoming % 2 == 1 then current or incoming else incoming
|
||||||
end)
|
end)
|
||||||
|
|
@ -192,10 +192,10 @@ test("should trigger effect even computed already dirty", function()
|
||||||
table.insert(calls, c2())
|
table.insert(calls, c2())
|
||||||
end)
|
end)
|
||||||
assert(table.concat(calls, ",") == "0foo", table.concat(calls, ","))
|
assert(table.concat(calls, ",") == "0foo", table.concat(calls, ","))
|
||||||
assert(v() == 1, v())
|
assert(v() == 1, `expected 1 for v(), got {v()}`)
|
||||||
v(2)
|
v(2)
|
||||||
assert(table.concat(calls, ",") == "0foo,2foo", table.concat(calls, ","))
|
assert(table.concat(calls, ",") == "0foo,2foo", table.concat(calls, ","))
|
||||||
assert(v() == 2, v())
|
assert(v() == 2, `expected 2 for v(), got {v()}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,7 @@ end)
|
||||||
test("should run the cleanup if the effect disposes itself", function()
|
test("should run the cleanup if the effect disposes itself", function()
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
local calls = 0
|
local calls = 0
|
||||||
local dispose
|
local dispose: () -> ()
|
||||||
|
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
if a() > 0 then
|
if a() > 0 then
|
||||||
|
|
@ -460,7 +460,7 @@ end)
|
||||||
test("should not run the effect if the cleanup function disposes it", function()
|
test("should not run the effect if the cleanup function disposes it", function()
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
local calls = 0
|
local calls = 0
|
||||||
local dispose
|
local dispose: () -> ()
|
||||||
|
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
a()
|
a()
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ end)
|
||||||
|
|
||||||
test("should run indirect scope cleanup", function()
|
test("should run indirect scope cleanup", function()
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
|
local cleanup: () -> ()
|
||||||
|
|
||||||
local cleanup
|
|
||||||
local stopScope = effectScope(function()
|
local stopScope = effectScope(function()
|
||||||
cleanup = effectScope(function()
|
cleanup = effectScope(function()
|
||||||
onCleanup(function()
|
onCleanup(function()
|
||||||
|
|
@ -50,8 +50,8 @@ end)
|
||||||
|
|
||||||
test("should run indirect effect cleanup", function()
|
test("should run indirect effect cleanup", function()
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
|
|
||||||
local a, setA = signal(false)
|
local a, setA = signal(false)
|
||||||
|
|
||||||
local stopScope = effectScope(function()
|
local stopScope = effectScope(function()
|
||||||
effect(function()
|
effect(function()
|
||||||
a()
|
a()
|
||||||
|
|
@ -73,7 +73,7 @@ end)
|
||||||
test("should dispose inner effects if created in an effect", function()
|
test("should dispose inner effects if created in an effect", function()
|
||||||
local source, setSource = signal(1)
|
local source, setSource = signal(1)
|
||||||
local triggers = 0
|
local triggers = 0
|
||||||
local dispose
|
local dispose: () -> ()
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
dispose = effectScope(function()
|
dispose = effectScope(function()
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ end
|
||||||
immediately closing it, causing alien-signals to unlink the computed value
|
immediately closing it, causing alien-signals to unlink the computed value
|
||||||
from its dependencies and allow it to be garbage collected.
|
from its dependencies and allow it to be garbage collected.
|
||||||
]]
|
]]
|
||||||
local function unlink(getter: () -> ())
|
local function unlink(getter: () -> unknown)
|
||||||
effectScope(function()
|
effectScope(function()
|
||||||
getter()
|
getter()
|
||||||
end)()
|
end)()
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ test("should handle recursive additions and removals", function()
|
||||||
|
|
||||||
observe(items, function(item)
|
observe(items, function(item)
|
||||||
if item < 5 then
|
if item < 5 then
|
||||||
setItems(function(state)
|
setItems(function(state: { number })
|
||||||
state = table.clone(state)
|
state = table.clone(state)
|
||||||
table.insert(state, item + 1)
|
table.insert(state, item + 1)
|
||||||
return state
|
return state
|
||||||
|
|
@ -117,7 +117,7 @@ test("should handle self removal", function()
|
||||||
local edits = {}
|
local edits = {}
|
||||||
|
|
||||||
observe(items, function(item, index)
|
observe(items, function(item, index)
|
||||||
setItems(function(state)
|
setItems(function(state: { number })
|
||||||
state = table.clone(state)
|
state = table.clone(state)
|
||||||
state[index] = nil
|
state[index] = nil
|
||||||
return state
|
return state
|
||||||
|
|
@ -134,7 +134,7 @@ end)
|
||||||
test("should allow disposal during addition", function()
|
test("should allow disposal during addition", function()
|
||||||
local items, setItems = signal({})
|
local items, setItems = signal({})
|
||||||
local edits = {}
|
local edits = {}
|
||||||
local dispose
|
local dispose: () -> ()
|
||||||
|
|
||||||
dispose = observe(items, function(item)
|
dispose = observe(items, function(item)
|
||||||
if item == 1 then
|
if item == 1 then
|
||||||
|
|
@ -153,7 +153,7 @@ end)
|
||||||
test("should allow disposal during removal", function()
|
test("should allow disposal during removal", function()
|
||||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||||
local edits = {}
|
local edits = {}
|
||||||
local dispose
|
local dispose: () -> ()
|
||||||
|
|
||||||
dispose = observe(items, function(item)
|
dispose = observe(items, function(item)
|
||||||
table.insert(edits, `+{item}`)
|
table.insert(edits, `+{item}`)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ local effect = Charm.effect
|
||||||
|
|
||||||
test("should accept equality function", function()
|
test("should accept equality function", function()
|
||||||
local calls = {}
|
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
|
return current == incoming or incoming % 2 == 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -442,18 +442,16 @@ end)
|
||||||
test.describe("error handling", function()
|
test.describe("error handling", function()
|
||||||
test("should keep graph consistent on errors during activation", function()
|
test("should keep graph consistent on errors during activation", function()
|
||||||
local a = atom(0)
|
local a = atom(0)
|
||||||
local b = computed(function()
|
local b = computed(function(): nil
|
||||||
error("fail")
|
error("fail")
|
||||||
end)
|
end)
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(b)
|
||||||
b()
|
|
||||||
end)
|
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(tostring(result), "fail"), "should throw fail")
|
||||||
|
|
||||||
a(1)
|
a(1)
|
||||||
assert(c() == 1, "c should recover and return 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()
|
test("should keep graph consistent on errors in computeds", function()
|
||||||
local a = atom(0)
|
local a = atom(0)
|
||||||
local b = computed(function()
|
local b = computed(function(): number
|
||||||
if a() == 1 then
|
if a() == 1 then
|
||||||
error("fail")
|
error("fail")
|
||||||
end
|
end
|
||||||
|
|
@ -474,11 +472,9 @@ test.describe("error handling", function()
|
||||||
assert(c() == 0, "c should be 0")
|
assert(c() == 0, "c should be 0")
|
||||||
|
|
||||||
a(1)
|
a(1)
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(b)
|
||||||
b()
|
|
||||||
end)
|
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(tostring(result), "fail"), "should throw fail")
|
||||||
|
|
||||||
a(2)
|
a(2)
|
||||||
assert(c() == 2, "c should recover and return 2")
|
assert(c() == 2, "c should recover and return 2")
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ end)
|
||||||
|
|
||||||
test("should run detached from effect scope", function()
|
test("should run detached from effect scope", function()
|
||||||
local effects = 0
|
local effects = 0
|
||||||
local stopEffect
|
local stopEffect: () -> ()
|
||||||
|
|
||||||
local stopScope = effectScope(function()
|
local stopScope = effectScope(function()
|
||||||
untracked(function()
|
untracked(function()
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,10 @@ test("should unwrap proxy assignments", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
src.a, src.b = src.a, src.b
|
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
|
src.a, src.b = src.b, src.a
|
||||||
assert(runs == 3, runs)
|
assert(runs == 3, `bad runs: {runs}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should track iteration", function()
|
test("should track iteration", function()
|
||||||
|
|
@ -125,10 +125,10 @@ test("should not track unread properties", function()
|
||||||
local _ = src.value.a
|
local _ = src.value.a
|
||||||
calls += 1
|
calls += 1
|
||||||
end)
|
end)
|
||||||
assert(calls == 1, calls)
|
assert(calls == 1, `bad calls: {calls}`)
|
||||||
|
|
||||||
src.value.b = 3
|
src.value.b = 3
|
||||||
assert(calls == 1, calls)
|
assert(calls == 1, `bad calls: {calls}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should overwrite with table passed to setter", function()
|
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(src) == raw, "unwrap did not return the original table")
|
||||||
assert(toRaw(raw) == raw, "unwrap modified a non-proxy value")
|
assert(toRaw(raw) == raw, "unwrap modified a non-proxy value")
|
||||||
assert(runs == 1, runs)
|
assert(runs == 1, `bad runs: {runs}`)
|
||||||
|
|
||||||
src.x = 1
|
src.x = 1
|
||||||
assert(runs == 2, runs)
|
assert(runs == 2, `bad runs: {runs}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should unwrap nested proxy", function()
|
test("should unwrap nested proxy", function()
|
||||||
|
|
@ -177,13 +177,13 @@ test("should unwrap nested proxy", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(toRaw(src) == raw, "unwrap did not return the original table")
|
assert(toRaw(src) == raw, "unwrap did not return the original table")
|
||||||
assert(runs == 1, runs)
|
assert(runs == 1, `bad runs: {runs}`)
|
||||||
|
|
||||||
inner.x = 1
|
inner.x = 1
|
||||||
assert(runs == 2, runs)
|
assert(runs == 2, `bad runs: {runs}`)
|
||||||
|
|
||||||
src.y = 2
|
src.y = 2
|
||||||
assert(runs == 3, runs)
|
assert(runs == 3, `bad runs: {runs}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ local React = require("./React")
|
||||||
@param dependencies An array of values used to memoize the getter.
|
@param dependencies An array of values used to memoize the getter.
|
||||||
@return The current state.
|
@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)
|
local value, setValue = React.useState(getter)
|
||||||
|
|
||||||
React.useLayoutEffect(function()
|
React.useLayoutEffect(function()
|
||||||
return Charm.listen(getter, setValue)
|
return Charm.listen(getter, setValue :: (T, T?) -> ())
|
||||||
end, dependencies or {})
|
end, dependencies :: { any } or {})
|
||||||
|
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
@ -34,13 +34,13 @@ end
|
||||||
@param dependencies An array of values used to memoize the getter.
|
@param dependencies An array of values used to memoize the getter.
|
||||||
@return A binding that stores the current state.
|
@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 initialValue = React.useMemo(getter, {})
|
||||||
local binding, setBinding = React.useBinding(initialValue)
|
local binding, setBinding = React.useBinding(initialValue)
|
||||||
|
|
||||||
React.useLayoutEffect(function()
|
React.useLayoutEffect(function()
|
||||||
return Charm.listen(getter, setBinding)
|
return Charm.listen(getter, setBinding :: (T, T?) -> ())
|
||||||
end, dependencies or {})
|
end, dependencies :: { any } or {})
|
||||||
|
|
||||||
return binding
|
return binding
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ local function renderHook<Props, Result>(callback: (Props) -> Result, props: Pro
|
||||||
|
|
||||||
local function Component(props: Props)
|
local function Component(props: Props)
|
||||||
hook.result = callback(props)
|
hook.result = callback(props)
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function hook.rerender(newProps: Props?)
|
function hook.rerender(newProps: Props?)
|
||||||
props = newProps or props
|
props = newProps or props
|
||||||
ReactRoblox.act(function()
|
ReactRoblox.act(function()
|
||||||
root:render(React.createElement(Component, props))
|
root:render(React.createElement(Component, props) :: any)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local test = require("../../tests/test")
|
local test = require("../../tests/test")
|
||||||
|
|
||||||
|
-- selene: allow(global_usage)
|
||||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||||
|
|
||||||
test.describe("useSignalState", function()
|
test.describe("useSignalState", function()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ local Vide = require("./Vide")
|
||||||
local function useSignalState<State>(getter: () -> State): () -> State
|
local function useSignalState<State>(getter: () -> State): () -> State
|
||||||
local state = Vide.source(getter())
|
local state = Vide.source(getter())
|
||||||
|
|
||||||
Vide.cleanup(Charm.listen(getter, state))
|
Vide.cleanup(Charm.listen(getter, state :: (State, State?) -> ()))
|
||||||
|
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ local test = require("../../../tests/test")
|
||||||
|
|
||||||
test("tracks signal state", function()
|
test("tracks signal state", function()
|
||||||
local getState, setState = Charm.signal(0)
|
local getState, setState = Charm.signal(0)
|
||||||
local source
|
local source: () -> number
|
||||||
local cleanup = Vide.root(function()
|
local cleanup = Vide.root(function()
|
||||||
source = VideCharm.useSignalState(getState)
|
source = VideCharm.useSignalState(getState)
|
||||||
end)
|
end)
|
||||||
|
|
@ -17,7 +17,7 @@ end)
|
||||||
|
|
||||||
test("accepts a selector", function()
|
test("accepts a selector", function()
|
||||||
local getState, setState = Charm.signal(0)
|
local getState, setState = Charm.signal(0)
|
||||||
local source
|
local source: () -> number
|
||||||
local cleanup = Vide.root(function()
|
local cleanup = Vide.root(function()
|
||||||
source = VideCharm.useSignalState(function()
|
source = VideCharm.useSignalState(function()
|
||||||
return 2 * getState()
|
return 2 * getState()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,4 @@ std = "roblox"
|
||||||
exclude = ["*/node_modules/*"]
|
exclude = ["*/node_modules/*"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
global_usage = "allow"
|
|
||||||
shadowing = "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