mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add toRaw()
This commit is contained in:
parent
f082956ed9
commit
9f66803f04
13 changed files with 297 additions and 198 deletions
104
README.md
104
README.md
|
|
@ -34,6 +34,8 @@
|
||||||
- https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity
|
- https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity
|
||||||
- https://angular.dev/guide/signals
|
- https://angular.dev/guide/signals
|
||||||
|
|
||||||
|
[Migrating from an older version?](#migration)
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Table of Contents</b></summary>
|
<summary><b>Table of Contents</b></summary>
|
||||||
|
|
||||||
|
|
@ -42,6 +44,7 @@
|
||||||
- [`signal(initialValue, equals?)`](#signalinitialvalue-equals)
|
- [`signal(initialValue, equals?)`](#signalinitialvalue-equals)
|
||||||
- [`computed(getter)`](#computedgetter)
|
- [`computed(getter)`](#computedgetter)
|
||||||
- [`effect(callback)`](#effectcallback)
|
- [`effect(callback)`](#effectcallback)
|
||||||
|
- [`reactive(initialValue)`](#reactiveinitialvalue)
|
||||||
- [`untracked(callback)`](#untrackedcallback)
|
- [`untracked(callback)`](#untrackedcallback)
|
||||||
- [`peek(callback)`](#peekcallback)
|
- [`peek(callback)`](#peekcallback)
|
||||||
- [`batch(callback)`](#batchcallback)
|
- [`batch(callback)`](#batchcallback)
|
||||||
|
|
@ -54,7 +57,6 @@
|
||||||
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
||||||
- [`recursive()`](#recursive)
|
- [`recursive()`](#recursive)
|
||||||
- [`trigger(callback)`](#triggercallback)
|
- [`trigger(callback)`](#triggercallback)
|
||||||
- [`reactive(initialValue)`](#reactiveinitialvalue)
|
|
||||||
- [`flags`](#flags)
|
- [`flags`](#flags)
|
||||||
- [Client-Server Sync](#client-server-sync)
|
- [Client-Server Sync](#client-server-sync)
|
||||||
- [Installation](#installation-1)
|
- [Installation](#installation-1)
|
||||||
|
|
@ -276,9 +278,50 @@ setCounter(2) -- Outer: 2, Inner: 2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### `reactive(initialValue)`
|
||||||
|
|
||||||
|
By default, Charm's signals are _shallowly reactive_, meaning only the value itself is reactive, and table properties are not tracked. Signal values must also be **immutable** in order to track changes in tables.
|
||||||
|
|
||||||
|
You can opt-in to deep reactivity with **reactive proxies**. The `reactive()` function takes a mutable table and wraps it in a reactive proxy. Reading properties through the proxy will perform dependency tracking, and table values will also be wrapped in a reactive proxy.
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local users, setUsers = reactive({
|
||||||
|
{ name = "John", surname = "Doe" },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Output: John Doe
|
||||||
|
effect(function()
|
||||||
|
print(`{users[1].name} {users[1].surname}`)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Output: Jane Smith
|
||||||
|
setUsers(function(state)
|
||||||
|
state[1].name = "Jane"
|
||||||
|
state[1].surname = "Smith"
|
||||||
|
table.insert(state, { name = "Steve", surname = "Doe" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- You can also mutate the reactive proxy directly:
|
||||||
|
users[1].name = "John" -- John Smith
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Because reactive proxies use metatables for reading and writing, functions like `table.insert` will not work on the proxy. Table functions should only be called on the raw table value.
|
||||||
|
|
||||||
|
If you need to access the raw table through the reactive proxy, use the `toRaw()` function:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local raw = {}
|
||||||
|
local proxy = reactive(raw)
|
||||||
|
|
||||||
|
print(toRaw(proxy) == raw) -- true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `untracked(callback)`
|
### `untracked(callback)`
|
||||||
|
|
||||||
In case you want to read signals but don't want to subscribe to them, you can use `untracked()` to essentially run code _outside_ the current effect, preventing signals and effects in the callback from being tracked.
|
In case you want to opt-out of dependency tracking in an effect, you can use `untracked()` to call a function _outside_ the current scope, preventing signals and effects in the function from being tracked.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getTracked, setTracked = signal(0)
|
local getTracked, setTracked = signal(0)
|
||||||
|
|
@ -294,7 +337,7 @@ setUntracked(1) -- No output
|
||||||
setTracked(2) -- Tracked: 2, Untracked: 1
|
setTracked(2) -- Tracked: 2, Untracked: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `untracked()` executes the callback outside the current effect, nested effects created in the callback do not get tracked by the parent effect:
|
Because `untracked()` executes the callback outside the current effect, nested effects created during the execution of the callback will not be tracked by the parent effect:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local stopEffect
|
local stopEffect
|
||||||
|
|
@ -602,45 +645,6 @@ print(total()) -- 2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `reactive(initialValue)`
|
|
||||||
|
|
||||||
The `reactive()` function creates a mutable reactive object that subscribes to only the properties you read. It also returns a setter function you can use to mutate the original table:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local users, setUsers = reactive({
|
|
||||||
user = { name = "John", surname = "Doe" },
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Output: John Doe
|
|
||||||
effect(function()
|
|
||||||
print(`{users.user.name} {users.user.surname}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Output: Jane Smith
|
|
||||||
setUsers(function(state)
|
|
||||||
state.user.name = "Jane"
|
|
||||||
state.user.surname = "Smith"
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also set properties on the reactive object directly:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local id = reactive({ name = "John", surname = "Doe" })
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
print(`{id.name} {id.surname}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
id.name = "Jane" -- Jane Doe
|
|
||||||
id.surname = "Smith" -- Jane Smith
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Be careful when mutating the reactive object! Because it's a proxy, functions like `table.insert` will not work. Table functions should be called on the original table, which is provided by the setter function.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `flags`
|
### `flags`
|
||||||
|
|
||||||
Charm exposes the following global flags to customize behavior:
|
Charm exposes the following global flags to customize behavior:
|
||||||
|
|
@ -948,7 +952,7 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
||||||
- Instead of creating client/server syncers, these modules now act like singletons. Sync APIs are called directly through `CharmSync.client`/`server`.
|
- Instead of creating client/server syncers, these modules now act like singletons. Sync APIs are called directly through `CharmSync.client`/`server`.
|
||||||
- [Read the updated docs for syncing state →](#client-server-sync)
|
- [Read the updated docs for syncing state →](#client-server-sync)
|
||||||
|
|
||||||
3. The [`strict` and `frozen` flags](#flags) are automatically enabled in Roblox Studio, so unsafe Charm code will start throwing errors if you didn't use the old `__DEV__` flag. The flags have the following behavior:
|
3. The [`strict` and `frozen` flags](#flags) are automatically enabled in Roblox Studio, so unsafe Charm code will start throwing errors. The flags have the following behaviors:
|
||||||
- `strict`: Yielding in effects, signals, and other critical Charm functions will throw an error
|
- `strict`: Yielding in effects, signals, and other critical Charm functions will throw an error
|
||||||
- `frozen`: Tables passed to signals are deeply frozen to strictly enforce data immutability and prevent accidental mutations
|
- `frozen`: Tables passed to signals are deeply frozen to strictly enforce data immutability and prevent accidental mutations
|
||||||
|
|
||||||
|
|
@ -966,14 +970,12 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
||||||
- Recursive checks are opt-out. You can allow recursion for a specific effect by calling [`recursive()`](#recursive) at the top of the effect callback.
|
- Recursive checks are opt-out. You can allow recursion for a specific effect by calling [`recursive()`](#recursive) at the top of the effect callback.
|
||||||
|
|
||||||
6. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
|
6. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
|
||||||
- Added [`signal()`](#signalinitialvalue-equals) to make reads and writes more explicit
|
- [`reactive()`](#reactiveinitialvalue): make mutable tables deeply reactive
|
||||||
- Added [`listen()`](#listengetter-callback) to create a subscription that runs once immediately
|
- [`signal()`](#signalinitialvalue-equals): make reads and writes more explicit
|
||||||
- Added [`effectScope()`](#effectscopecallback) for collecting and cleaning up multiple effects at once
|
- [`listen()`](#listengetter-callback): create a subscription that runs once immediately
|
||||||
- Added [`onCleanup()`](#oncleanupcallback-failsilently) to bind a cleanup function to the active effect
|
- [`effectScope()`](#effectscopecallback): collect and clean up multiple effects at once
|
||||||
- Added [`trigger()`](#triggercallback) to trigger updates for table mutations
|
- [`onCleanup()`](#oncleanupcallback-failsilently): bind a cleanup function to the active effect
|
||||||
- The [`computed()`](#computedgetter) callback now gets called with the previous computed value
|
- [`trigger()`](#triggercallback): trigger updates for table mutations
|
||||||
- Optimized `computed()` to use lazy evaluation instead of eager updates
|
|
||||||
- Optimized `mapped()` to only map values that changed in the original table
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ end
|
||||||
local Charm = require(script.Parent.Parent.charm.src)
|
local Charm = require(script.Parent.Parent.charm.src)
|
||||||
|
|
||||||
export type Atom<T = any> = Charm.Atom<T>
|
export type Atom<T = any> = Charm.Atom<T>
|
||||||
|
export type ReactiveProxy<T = any> = Charm.ReactiveProxy<T>
|
||||||
|
|
||||||
return Charm
|
return Charm
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ local suite = require("../../../../test/suite")
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
local afterEach = suite.afterEach
|
local afterEach = suite.afterEach
|
||||||
|
|
||||||
local function mockArraySerdes(object: { [any]: any }): { [any]: any }
|
local function mockArraySerdes(object: {}): {}
|
||||||
if object[1] then
|
if object[1] then
|
||||||
local copy = {}
|
local copy = {}
|
||||||
for index = 1, #object do
|
for index = 1, #object do
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ local Charm = require("../Charm")
|
||||||
local patch = require("./patch")
|
local patch = require("./patch")
|
||||||
local types = require("./types")
|
local types = require("./types")
|
||||||
|
|
||||||
|
type ReactiveProxy<T = {}> = Charm.ReactiveProxy<T>
|
||||||
type SyncPayload = types.SyncPayload
|
type SyncPayload = types.SyncPayload
|
||||||
|
|
||||||
type SignalSetter<T = any> = (update: ((T) -> T) | T) -> ()
|
type SignalSetter<T = any> = (T | (T) -> T) -> ()
|
||||||
|
type ReactiveSetter<T = any> = (T | (T) -> ()) -> ()
|
||||||
type ReactiveSetter<T = any> = (update: ((T) -> ()) | T) -> ()
|
|
||||||
|
|
||||||
type Reactive<T = any> = typeof(setmetatable({}, {} :: { setValue: ReactiveSetter<T> }))
|
|
||||||
|
|
||||||
local trackedSignals: { [string]: SignalSetter } = {}
|
local trackedSignals: { [string]: SignalSetter } = {}
|
||||||
local trackedReactives: { [string]: ReactiveSetter } = {}
|
local trackedReactives: { [string]: ReactiveSetter } = {}
|
||||||
|
|
@ -24,12 +22,12 @@ local trackedReactives: { [string]: ReactiveSetter } = {}
|
||||||
|
|
||||||
@param signals A map of setter functions to sync with the server.
|
@param signals A map of setter functions to sync with the server.
|
||||||
]]
|
]]
|
||||||
local function addSignals(signals: { [string]: SignalSetter | { [any]: any } })
|
local function addSignals(signals: { [string]: SignalSetter | {} })
|
||||||
for key, signal in signals do
|
for key, signal in signals do
|
||||||
if type(signal) == "function" then
|
if type(signal) == "function" then
|
||||||
trackedSignals[key] = signal
|
trackedSignals[key] = signal
|
||||||
else
|
else
|
||||||
trackedReactives[key] = getmetatable((signal :: any) :: Reactive).setValue
|
trackedReactives[key] = getmetatable(signal :: ReactiveProxy).set
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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: { any }): { [string | number]: any }
|
local function stringifySparseArray(object: {}): { [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
|
||||||
|
|
@ -65,7 +65,7 @@ local function diff(oldState: any, newState: any): any
|
||||||
return newState
|
return newState
|
||||||
end
|
end
|
||||||
|
|
||||||
local diffs: { [any]: any } = table.clone(newState)
|
local diffs = table.clone(newState)
|
||||||
|
|
||||||
for key, oldValue in oldState do
|
for key, oldValue in oldState do
|
||||||
diffs[key] = diff(oldValue, newState[key])
|
diffs[key] = diff(oldValue, newState[key])
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,19 @@ local patch = require("./patch")
|
||||||
local types = require("./types")
|
local types = require("./types")
|
||||||
local utils = require("./utils")
|
local utils = require("./utils")
|
||||||
|
|
||||||
|
type ReactiveProxy<T = {}> = Charm.ReactiveProxy<T>
|
||||||
type SyncPayload = types.SyncPayload
|
type SyncPayload = types.SyncPayload
|
||||||
|
|
||||||
type SyncStatus = number
|
type SyncStatus = number
|
||||||
|
|
||||||
type ClientKeys = { [string]: SyncStatus }
|
type ClientKeys = { [string]: SyncStatus }
|
||||||
|
|
||||||
type Reactive<T> = typeof(setmetatable({}, {} :: { initialValue: T, track: () -> () }))
|
|
||||||
|
|
||||||
type TrackedState<T = any> = {
|
type TrackedState<T = any> = {
|
||||||
target: any,
|
target: any,
|
||||||
currentValue: T,
|
currentValue: T,
|
||||||
lastSyncedValue: T,
|
lastSyncedValue: T,
|
||||||
dispose: () -> (),
|
|
||||||
isReactive: boolean?,
|
isReactive: boolean?,
|
||||||
|
dispose: () -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateUpdate = {
|
type StateUpdate = {
|
||||||
|
|
@ -68,21 +67,21 @@ local function trackSignal<T>(key: string, getter: () -> T)
|
||||||
trackedStates[key] = trackedState
|
trackedStates[key] = trackedState
|
||||||
end
|
end
|
||||||
|
|
||||||
local function trackReactive<T>(key: string, reactive: Reactive<T>)
|
local function trackReactive<T>(key: string, proxy: ReactiveProxy<T>)
|
||||||
if trackedStates[key] then
|
if trackedStates[key] then
|
||||||
if trackedStates[key].target ~= reactive then
|
if trackedStates[key].target ~= proxy then
|
||||||
warn("[Charm] Attempted to sync different states with the same key:", key)
|
warn("[Charm] Attempted to sync different states with the same key:", key)
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local meta = getmetatable(reactive)
|
local meta = getmetatable(proxy)
|
||||||
local snapshot = utils.deepCopy(meta.initialValue)
|
local snapshot = utils.deepCopy(meta.value)
|
||||||
local firstRun = true
|
local firstRun = true
|
||||||
|
|
||||||
local trackedState = {
|
local trackedState = {
|
||||||
target = reactive,
|
target = proxy,
|
||||||
currentValue = meta.initialValue,
|
currentValue = meta.value,
|
||||||
lastSyncedValue = snapshot,
|
lastSyncedValue = snapshot,
|
||||||
isReactive = true,
|
isReactive = true,
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +92,7 @@ local function trackReactive<T>(key: string, reactive: Reactive<T>)
|
||||||
firstRun = false
|
firstRun = false
|
||||||
else
|
else
|
||||||
local oldSnapshot = snapshot
|
local oldSnapshot = snapshot
|
||||||
snapshot = utils.deepCopy(meta.initialValue)
|
snapshot = utils.deepCopy(meta.value)
|
||||||
table.insert(globalUpdates, {
|
table.insert(globalUpdates, {
|
||||||
key = key,
|
key = key,
|
||||||
newValue = snapshot,
|
newValue = snapshot,
|
||||||
|
|
@ -258,7 +257,7 @@ end
|
||||||
@param client The client receiving state updates.
|
@param client The client receiving state updates.
|
||||||
@param signals A map of getter functions to sync with the client.
|
@param signals A map of getter functions to sync with the client.
|
||||||
]]
|
]]
|
||||||
local function addSignalsToClient(client: any, signals: { [string]: { [any]: any } | () -> any })
|
local function addSignalsToClient(client: any, signals: { [string]: {} | () -> any })
|
||||||
if not clients[client] then
|
if not clients[client] then
|
||||||
clients[client] = {}
|
clients[client] = {}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ type NestedTable = {
|
||||||
[any]: NestedTable | any,
|
[any]: NestedTable | any,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlattenedTable = {
|
|
||||||
[any]: 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.
|
||||||
|
|
@ -17,15 +13,15 @@ type FlattenedTable = {
|
||||||
@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(input: NestedTable): FlattenedTable
|
local function flatten<Flattened>(input: {}): Flattened
|
||||||
local result: FlattenedTable = {}
|
local result = {}
|
||||||
|
|
||||||
local function visit(node: NestedTable, path: string)
|
local function visit(node: {}, path: string)
|
||||||
for key, value in node do
|
for key, value in node do
|
||||||
local location = if path == "" then key else path .. "/" .. key
|
local location = if path == "" then tostring(key) else path .. "/" .. tostring(key)
|
||||||
|
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
visit(value :: NestedTable, location)
|
visit(value :: {}, location)
|
||||||
else
|
else
|
||||||
result[location] = value
|
result[location] = value
|
||||||
end
|
end
|
||||||
|
|
@ -34,7 +30,7 @@ local function flatten(input: NestedTable): FlattenedTable
|
||||||
|
|
||||||
visit(input, "")
|
visit(input, "")
|
||||||
|
|
||||||
return result
|
return result :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
local function isTableUnsafe(object: { [any]: any })
|
local function isTableUnsafe(object: {})
|
||||||
local key = next(object)
|
local key = next(object)
|
||||||
|
|
||||||
if key == nil then
|
if key == nil then
|
||||||
|
|
@ -12,7 +12,7 @@ local function isTableUnsafe(object: { [any]: any })
|
||||||
for key in object do
|
for key in object do
|
||||||
if type(key) ~= keyType then
|
if type(key) ~= keyType then
|
||||||
return true
|
return true
|
||||||
elseif keyType == "number" and key > maxKey then
|
elseif type(key) == "number" and key > maxKey then
|
||||||
maxKey = key
|
maxKey = key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -33,7 +33,7 @@ end
|
||||||
@param value The table value.
|
@param value The table value.
|
||||||
@param key The key of the value.
|
@param key The key of the value.
|
||||||
]]
|
]]
|
||||||
local function validate(value: { [any]: any })
|
local function validate(value: {})
|
||||||
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`
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
local Charm = require("../")
|
local Charm = require("../")
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local atom = Charm.atom
|
type ReactiveProxy<T = any> = Charm.ReactiveProxy<T>
|
||||||
|
|
||||||
|
local signal = Charm.signal
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local effectScope = Charm.effectScope
|
local effectScope = Charm.effectScope
|
||||||
|
local reactive = Charm.reactive
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
--[[
|
|
||||||
Creates a weak reference to a value.
|
|
||||||
]]
|
|
||||||
local function weakRef<T>(value: T): { value: T? }
|
local function weakRef<T>(value: T): { value: T? }
|
||||||
return setmetatable({ value = value }, { __mode = "v" }) :: any
|
return setmetatable({ value = value }, { __mode = "v" }) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
|
||||||
Triggers garbage collection by creating a large number of tables.
|
|
||||||
]]
|
|
||||||
local function gc()
|
local function gc()
|
||||||
for _ = 1, 2000 do
|
for _ = 1, 2000 do
|
||||||
local _ = table.create(100, 0)
|
local _ = table.create(100, 0)
|
||||||
|
|
@ -41,14 +38,14 @@ end
|
||||||
|
|
||||||
test("should release computed cache", function()
|
test("should release computed cache", function()
|
||||||
local srcRef = weakRef({})
|
local srcRef = weakRef({})
|
||||||
local src = atom(srcRef.value)
|
local src, setSrc = signal(srcRef.value)
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return src()
|
return src()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
c() -- cache src value
|
c() -- cache src value
|
||||||
src(nil) -- release value
|
setSrc(nil) -- release value
|
||||||
c = unlink(c) -- release computed
|
unlink(c) -- release computed
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
assert(srcRef.value == nil, "should release computed cache")
|
assert(srcRef.value == nil, "should release computed cache")
|
||||||
|
|
@ -56,7 +53,7 @@ end)
|
||||||
|
|
||||||
test("should not release effect for atom", function()
|
test("should not release effect for atom", function()
|
||||||
local effectCalls = 0
|
local effectCalls = 0
|
||||||
local src = atom(0)
|
local src, setSrc = signal(0)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
effectCalls += 1
|
effectCalls += 1
|
||||||
|
|
@ -65,8 +62,52 @@ test("should not release effect for atom", function()
|
||||||
assert(effectCalls == 1, "should call effect once")
|
assert(effectCalls == 1, "should call effect once")
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
src(src() + 1)
|
setSrc(src() + 1)
|
||||||
assert(effectCalls == 2, "should call effect again")
|
assert(effectCalls == 2, "should call effect again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should release reactive tables", function()
|
||||||
|
local valueRef = weakRef({})
|
||||||
|
local src = reactive({ value = valueRef.value })
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
local _ = src.value -- create proxy
|
||||||
|
end)
|
||||||
|
src.value = nil :: any -- release proxy
|
||||||
|
|
||||||
|
gc()
|
||||||
|
assert(valueRef.value == nil, "should release reactive table")
|
||||||
|
assert((src :: ReactiveProxy).__signals.value == nil, "should release reactive signals")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should release reactive property deps", function()
|
||||||
|
local src = reactive({ foo = 1 })
|
||||||
|
|
||||||
|
local c = computed(function()
|
||||||
|
return src.foo
|
||||||
|
end)
|
||||||
|
|
||||||
|
c()
|
||||||
|
assert((src :: ReactiveProxy).__signals.foo ~= nil, "should have deps for foo")
|
||||||
|
|
||||||
|
c = nil :: any
|
||||||
|
gc()
|
||||||
|
assert((src :: ReactiveProxy).__signals.foo == nil, "should release deps for foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should not release effect for reactive", function()
|
||||||
|
local src = reactive({ foo = 1 })
|
||||||
|
local calls = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
calls += 1
|
||||||
|
local _ = src.foo
|
||||||
|
end)
|
||||||
|
assert(calls == 1, "should call effect once")
|
||||||
|
|
||||||
|
gc()
|
||||||
|
src.foo += 1
|
||||||
|
assert(calls == 2, "should call effect again")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local reactive = Charm.reactive
|
local reactive = Charm.reactive
|
||||||
|
local toRaw = Charm.toRaw
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should track nested properties", function()
|
test("should track nested properties", function()
|
||||||
|
|
@ -121,4 +122,22 @@ test("should overwrite with table passed to setter", function()
|
||||||
assert(table.concat(calls, ",") == "123,2nil4", table.concat(calls, ","))
|
assert(table.concat(calls, ",") == "123,2nil4", table.concat(calls, ","))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should unwrap raw table", function()
|
||||||
|
local raw = {}
|
||||||
|
local src = reactive(raw)
|
||||||
|
local runs = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
local _ = toRaw(src)
|
||||||
|
runs += 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
src.x = 1
|
||||||
|
assert(runs == 2, runs)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
13
packages/charm/src/index.d.ts
vendored
13
packages/charm/src/index.d.ts
vendored
|
|
@ -302,14 +302,23 @@ export function recursive(): void;
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param initialValue The value to make deeply reactive.
|
* @param initialValue The value to make deeply reactive.
|
||||||
* @return A proxy table that tracks reads to nested properties.
|
* @returns A proxy table and a function to mutate the original table.
|
||||||
* @return A function for updating the original table.
|
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
*/
|
*/
|
||||||
export function reactive<T extends object>(
|
export function reactive<T extends object>(
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
): LuaTuple<[value: T, setter: (update: T | ((initialValue: T) => void)) => T]>;
|
): LuaTuple<[value: T, setter: (update: T | ((initialValue: T) => void)) => T]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwraps a reactive proxy to get the original table. If the provided value
|
||||||
|
* is not a proxy, it is returned as-is.
|
||||||
|
*
|
||||||
|
* @param value A reactive proxy or a non-proxy value.
|
||||||
|
* @returns The unwrapped target value.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
|
*/
|
||||||
|
export function toRaw<T>(value: T): T;
|
||||||
|
|
||||||
export function getActiveSub(): ReactiveNode | undefined;
|
export function getActiveSub(): ReactiveNode | undefined;
|
||||||
|
|
||||||
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined;
|
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined;
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,19 @@ export type Link = system.Link
|
||||||
|
|
||||||
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
||||||
|
|
||||||
type Target = { [any]: any }
|
type ReactiveProxyMeta<T = {}> = {
|
||||||
|
value: T,
|
||||||
type Proxy = {
|
track: () -> (),
|
||||||
__target: Target,
|
set: (T | (T) -> ()?) -> (),
|
||||||
__signals: { [any]: () -> any },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReactiveProxyProps<T = {}> = {
|
||||||
|
__target: T,
|
||||||
|
__signals: { [unknown]: () -> unknown },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReactiveProxy<T = {}> = T & typeof(setmetatable({} :: ReactiveProxyProps<T>, {} :: ReactiveProxyMeta<T>))
|
||||||
|
|
||||||
type EffectScopeNode = ReactiveNode & {
|
type EffectScopeNode = ReactiveNode & {
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +141,7 @@ local function runCleanups(effect: EffectScopeNode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(e: EffectNode): ()
|
local function run(e: EffectNode)
|
||||||
local flags = e.flags
|
local flags = e.flags
|
||||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(e.deps :: Link, e)) then
|
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(e.deps :: Link, e)) then
|
||||||
runCleanups(e)
|
runCleanups(e)
|
||||||
|
|
@ -157,7 +163,7 @@ local function run(e: EffectNode): ()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush(): ()
|
local function flush()
|
||||||
while notifyIndex < queuedLength do
|
while notifyIndex < queuedLength do
|
||||||
local effect = queued[notifyIndex] :: EffectNode
|
local effect = queued[notifyIndex] :: EffectNode
|
||||||
queued[notifyIndex] = nil
|
queued[notifyIndex] = nil
|
||||||
|
|
@ -320,6 +326,50 @@ local function effectOper(this: EffectScopeNode): () -> ()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function update(node: ReactiveNode): boolean
|
||||||
|
if node.depsTail then
|
||||||
|
return updateComputed(node :: ComputedNode)
|
||||||
|
else
|
||||||
|
return updateSignal(node :: SignalNode)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function notify(effect: ReactiveNode)
|
||||||
|
local insertIndex = queuedLength
|
||||||
|
local firstInsertedIndex = insertIndex
|
||||||
|
|
||||||
|
repeat
|
||||||
|
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
|
||||||
|
queued[insertIndex] = effect :: EffectNode
|
||||||
|
insertIndex += 1
|
||||||
|
effect = (effect.subs and effect.subs.sub) :: EffectNode
|
||||||
|
if not effect or bit32.band(effect.flags, WATCHING) == 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
until false
|
||||||
|
|
||||||
|
queuedLength = insertIndex
|
||||||
|
|
||||||
|
insertIndex -= 1
|
||||||
|
while firstInsertedIndex < insertIndex do
|
||||||
|
local left = queued[firstInsertedIndex]
|
||||||
|
queued[firstInsertedIndex] = queued[insertIndex]
|
||||||
|
queued[insertIndex] = left
|
||||||
|
firstInsertedIndex += 1
|
||||||
|
insertIndex -= 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unwatched(node: ReactiveNode)
|
||||||
|
if bit32.band(node.flags, MUTABLE) == 0 then
|
||||||
|
effectOper(node :: EffectScopeNode)()
|
||||||
|
elseif node.depsTail then
|
||||||
|
node.depsTail = nil
|
||||||
|
node.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
|
purgeDeps(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Creates a reactive signal that stores a value and notifies subscribers when
|
Creates a reactive signal that stores a value and notifies subscribers when
|
||||||
the value changes. The signal provides both a getter and a setter.
|
the value changes. The signal provides both a getter and a setter.
|
||||||
|
|
@ -590,6 +640,7 @@ end
|
||||||
local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): () -> ()
|
local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): () -> ()
|
||||||
local tracked = toSignal(getter)
|
local tracked = toSignal(getter)
|
||||||
local current: T
|
local current: T
|
||||||
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
|
|
@ -614,6 +665,7 @@ local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ())
|
||||||
local tracked = toSignal(getter)
|
local tracked = toSignal(getter)
|
||||||
local current: T
|
local current: T
|
||||||
local firstRun = true
|
local firstRun = true
|
||||||
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
|
|
@ -673,6 +725,7 @@ local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) ->
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for key, value in data do
|
for key, value in data do
|
||||||
if innerScopes[key] == nil then
|
if innerScopes[key] == nil then
|
||||||
local disposeScope = effectScope(function()
|
local disposeScope = effectScope(function()
|
||||||
|
|
@ -696,6 +749,7 @@ local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) ->
|
||||||
-- interfere with the current update cycle.
|
-- interfere with the current update cycle.
|
||||||
batch(updateScopes, getter())
|
batch(updateScopes, getter())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
if disposed then
|
if disposed then
|
||||||
return
|
return
|
||||||
|
|
@ -781,16 +835,16 @@ end
|
||||||
@example
|
@example
|
||||||
```luau
|
```luau
|
||||||
local users, setUsers = reactive({
|
local users, setUsers = reactive({
|
||||||
user = { name = "John", surname = "Doe" },
|
{ name = "John", surname = "Doe" },
|
||||||
})
|
})
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
print(users.user and `{users.user.name} {users.user.surname}`)
|
print(`{users[1].name} {users[1].surname}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setUsers(function(state)
|
setUsers(function(state)
|
||||||
state.user.name = "Jane"
|
state[1].name = "Jane"
|
||||||
state.user.surname = "Smith"
|
state[1].surname = "Smith"
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -801,53 +855,32 @@ end
|
||||||
]]
|
]]
|
||||||
local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -> ())
|
local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -> ())
|
||||||
local track = signal(nil)
|
local track = signal(nil)
|
||||||
|
local proxyMeta = { value = initialValue, track = track }
|
||||||
|
local proxyMap = setmetatable({} :: { [{}]: any }, { __mode = "v" })
|
||||||
|
|
||||||
local function setValue(update: ((T) -> ()) | T?)
|
local function trackProperty(cache: { [any]: () -> any }, target: {}, key: any)
|
||||||
if type(update) == "function" then
|
if cache[key] then
|
||||||
update(initialValue)
|
return cache[key]()
|
||||||
elseif type(update) == "table" then
|
|
||||||
table.clear(initialValue)
|
|
||||||
for key, value in update do
|
|
||||||
(initialValue :: Target)[key] = value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
trigger(track)
|
cache[key] = computed(function()
|
||||||
|
track()
|
||||||
|
return target[key]
|
||||||
|
end)
|
||||||
|
return cache[key]()
|
||||||
end
|
end
|
||||||
|
|
||||||
local proxyMeta = {
|
local function createProxy<T>(target: T & {}): ReactiveProxy<T>
|
||||||
initialValue = initialValue,
|
if proxyMap[target] then
|
||||||
track = track,
|
return proxyMap[target]
|
||||||
setValue = setValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
local proxyMap = setmetatable({} :: { [Target]: any }, { __mode = "k" })
|
|
||||||
|
|
||||||
local function trackProperty(cache: { [any]: () -> any }, target: Target, key: any)
|
|
||||||
if not cache[key] then
|
|
||||||
cache[key] = untracked(computed, function()
|
|
||||||
track()
|
|
||||||
return target[key]
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
cache[key]()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function createProxy(target: Target): Proxy
|
|
||||||
if not proxyMap[target] then
|
|
||||||
proxyMap[target] = setmetatable({
|
|
||||||
__target = target,
|
|
||||||
__signals = setmetatable({}, { __mode = "v" }),
|
|
||||||
}, proxyMeta)
|
|
||||||
end
|
end
|
||||||
|
proxyMap[target] = setmetatable({
|
||||||
|
__target = target,
|
||||||
|
__signals = setmetatable({}, { __mode = "v" }),
|
||||||
|
}, proxyMeta)
|
||||||
return proxyMap[target]
|
return proxyMap[target]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function nextProxy(target: Target, index: any)
|
function proxyMeta.__index(self: ReactiveProxy, key: any): any
|
||||||
local nextKey, nextValue = next(target, index)
|
|
||||||
return nextKey, if type(nextValue) == "table" then createProxy(nextValue) else nextValue
|
|
||||||
end
|
|
||||||
|
|
||||||
function proxyMeta.__index(self: Proxy, key: any): any
|
|
||||||
if activeSub then
|
if activeSub then
|
||||||
trackProperty(self.__signals, self.__target, key)
|
trackProperty(self.__signals, self.__target, key)
|
||||||
end
|
end
|
||||||
|
|
@ -855,7 +888,7 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
|
||||||
return if type(value) == "table" then createProxy(value) else value
|
return if type(value) == "table" then createProxy(value) else value
|
||||||
end
|
end
|
||||||
|
|
||||||
function proxyMeta.__newindex(self: Proxy, key: any, value: any)
|
function proxyMeta.__newindex(self: ReactiveProxy, key: any, value: any)
|
||||||
if type(value) == "table" and getmetatable(value) == proxyMeta then
|
if type(value) == "table" and getmetatable(value) == proxyMeta then
|
||||||
value = value.__target
|
value = value.__target
|
||||||
end
|
end
|
||||||
|
|
@ -865,61 +898,61 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function proxyMeta.__iter(self: Proxy)
|
function proxyMeta.__iter(self: ReactiveProxy)
|
||||||
track()
|
track()
|
||||||
return nextProxy, self.__target
|
local function iterator(target: {}, index: any)
|
||||||
|
local key, value = next(target, index)
|
||||||
|
return key, if type(value) == "table" then createProxy(value) else value
|
||||||
|
end
|
||||||
|
return iterator, self.__target
|
||||||
end
|
end
|
||||||
|
|
||||||
function proxyMeta.__len(self: Proxy)
|
function proxyMeta.__len(self: ReactiveProxy)
|
||||||
track()
|
track()
|
||||||
return #self.__target
|
return #self.__target
|
||||||
end
|
end
|
||||||
|
|
||||||
return createProxy(initialValue) :: any, setValue
|
function proxyMeta.set(update: ((T) -> ()) | T?)
|
||||||
|
if type(update) == "function" then
|
||||||
|
update(initialValue)
|
||||||
|
elseif type(update) == "table" then
|
||||||
|
table.clear(initialValue)
|
||||||
|
for key, value in update :: {} do
|
||||||
|
(initialValue :: {})[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
trigger(track)
|
||||||
|
end
|
||||||
|
|
||||||
|
return createProxy(initialValue), proxyMeta.set
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Unwraps a reactive proxy to get the original table. If the provided value
|
||||||
|
is not a proxy, it is returned as-is.
|
||||||
|
|
||||||
|
@param value A reactive proxy or a non-proxy value.
|
||||||
|
@return The unwrapped target value.
|
||||||
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
|
]]
|
||||||
|
local function toRaw<T>(value: T): T
|
||||||
|
if type(value) == "table" and getmetatable(value) ~= nil then
|
||||||
|
local proxy: ReactiveProxy = value
|
||||||
|
local meta = getmetatable(proxy)
|
||||||
|
if meta.value == proxy.__target then
|
||||||
|
-- Track the root proxy since nested proxies are already tracked
|
||||||
|
meta.track()
|
||||||
|
end
|
||||||
|
return proxy.__target :: any
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local reactiveSystem = system.createReactiveSystem({
|
local reactiveSystem = system.createReactiveSystem({
|
||||||
update = function(node)
|
update = update,
|
||||||
if node.depsTail then
|
notify = notify,
|
||||||
return updateComputed(node :: ComputedNode)
|
unwatched = unwatched,
|
||||||
else
|
|
||||||
return updateSignal(node :: SignalNode)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
notify = function(effect)
|
|
||||||
local insertIndex = queuedLength
|
|
||||||
local firstInsertedIndex = insertIndex
|
|
||||||
|
|
||||||
repeat
|
|
||||||
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
|
|
||||||
queued[insertIndex] = effect :: EffectNode
|
|
||||||
insertIndex += 1
|
|
||||||
effect = (effect.subs and effect.subs.sub) :: EffectNode
|
|
||||||
if not effect or bit32.band(effect.flags, WATCHING) == 0 then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
until false
|
|
||||||
|
|
||||||
queuedLength = insertIndex
|
|
||||||
|
|
||||||
insertIndex -= 1
|
|
||||||
while firstInsertedIndex < insertIndex do
|
|
||||||
local left = queued[firstInsertedIndex]
|
|
||||||
queued[firstInsertedIndex] = queued[insertIndex]
|
|
||||||
queued[insertIndex] = left
|
|
||||||
firstInsertedIndex += 1
|
|
||||||
insertIndex -= 1
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
unwatched = function(node)
|
|
||||||
if bit32.band(node.flags, MUTABLE) == 0 then
|
|
||||||
effectOper(node :: EffectScopeNode)()
|
|
||||||
elseif node.depsTail then
|
|
||||||
node.depsTail = nil
|
|
||||||
node.flags = bit32.bor(MUTABLE, DIRTY)
|
|
||||||
purgeDeps(node)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
link = reactiveSystem.link
|
link = reactiveSystem.link
|
||||||
|
|
@ -954,4 +987,5 @@ return {
|
||||||
mapper: (item: VI, key: KI) -> (VO, KO)
|
mapper: (item: VI, key: KI) -> (VO, KO)
|
||||||
) -> () -> { [KO]: VO }) & (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }),
|
) -> () -> { [KO]: VO }) & (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }),
|
||||||
reactive = reactive,
|
reactive = reactive,
|
||||||
|
toRaw = toRaw,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ local function createReactiveSystem(props: {
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number): ()
|
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number)
|
||||||
-- DEVIATION: Do not link signals to peeking subscribers
|
-- DEVIATION: Do not link signals to peeking subscribers
|
||||||
if bit32.btest(sub.flags, PEEKING) and bit32.btest(dep.flags, MUTABLE) then
|
if bit32.btest(sub.flags, PEEKING) and bit32.btest(dep.flags, MUTABLE) then
|
||||||
return
|
return
|
||||||
|
|
@ -166,7 +166,7 @@ local function createReactiveSystem(props: {
|
||||||
return nextDep
|
return nextDep
|
||||||
end
|
end
|
||||||
|
|
||||||
local function propagate(link: Link): ()
|
local function propagate(link: Link)
|
||||||
repeat
|
repeat
|
||||||
local sub = link.sub
|
local sub = link.sub
|
||||||
local flags = sub.flags
|
local flags = sub.flags
|
||||||
|
|
@ -200,7 +200,7 @@ local function createReactiveSystem(props: {
|
||||||
until not link
|
until not link
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shallowPropagate(link: Link): ()
|
local function shallowPropagate(link: Link)
|
||||||
repeat
|
repeat
|
||||||
local sub = link.sub
|
local sub = link.sub
|
||||||
local flags = sub.flags
|
local flags = sub.flags
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue