mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add peek and roblox-ts types
This commit is contained in:
parent
d2979ebd4c
commit
708bc55de7
37 changed files with 363 additions and 1234 deletions
|
|
@ -1,24 +1,23 @@
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
suite.describe("charm-sync", function()
|
suite.describe("client", function()
|
||||||
if not game then
|
require("./client.spec")
|
||||||
return suite.skip()
|
end)
|
||||||
end
|
|
||||||
suite.describe("client", function()
|
suite.describe("server", function()
|
||||||
require("./client.spec")
|
require("./server.spec")
|
||||||
end)
|
end)
|
||||||
suite.describe("server", function()
|
|
||||||
require("./server.spec")
|
suite.describe("flatten", function()
|
||||||
end)
|
require("./flatten.spec")
|
||||||
suite.describe("flatten", function()
|
end)
|
||||||
require("./flatten.spec")
|
|
||||||
end)
|
suite.describe("patch", function()
|
||||||
suite.describe("patch", function()
|
require("./patch.spec")
|
||||||
require("./patch.spec")
|
end)
|
||||||
end)
|
|
||||||
suite.describe("validate", function()
|
suite.describe("validate", function()
|
||||||
require("./validate.spec")
|
require("./validate.spec")
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
local Charm = require("../Charm")
|
local Charm = require("../Charm")
|
||||||
local validate = require("./validate")
|
local validate = require("./validate")
|
||||||
|
|
||||||
|
local flags = Charm.flags
|
||||||
|
|
||||||
--- A special symbol that denotes the absence of a value. Used to represent
|
--- A special symbol that denotes the absence of a value. Used to represent
|
||||||
--- deleted values in patches.
|
--- deleted values in patches.
|
||||||
local None = { __none = "__none" }
|
local None = { __none = "__none" }
|
||||||
|
|
@ -63,7 +65,7 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serial
|
||||||
patches = stringifySparseArray(patches)
|
patches = stringifySparseArray(patches)
|
||||||
end
|
end
|
||||||
|
|
||||||
if serialize and Charm.strict.enabled then
|
if serialize and flags.strict then
|
||||||
for key, value in next, patches do
|
for key, value in next, patches do
|
||||||
validate(value, key)
|
validate(value, key)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ local function gc()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- In alien-core, a computed value unlinks from dependencies when its last
|
--- In alien-signals, a computed value unlinks from dependencies when its last
|
||||||
--- parent scope is disposed. Calling a computed value outside of a scope links
|
--- parent scope is disposed. Calling a computed value outside of a scope links
|
||||||
--- it to dependencies, but the dependencies hold strong references to the
|
--- it to dependencies, but the dependencies hold strong references to the
|
||||||
--- computed value, so it won't GC until called in a scope and disposed.
|
--- computed value, so it won't GC until called in a scope and disposed.
|
||||||
---
|
---
|
||||||
--- This function releases a computed value by linking it to a scope and then
|
--- This function releases a computed value by linking it to a scope and then
|
||||||
--- immediately closing it, causing alien-core to unlink the computed value
|
--- immediately closing it, causing alien-signals to unlink the computed value
|
||||||
--- and allow it to be garbage collected.
|
--- and allow it to be garbage collected.
|
||||||
local function release(getter: () -> ())
|
local function release(getter: () -> ())
|
||||||
effectScope(function()
|
effectScope(function()
|
||||||
|
|
|
||||||
107
packages/charm/src/__tests__/peek.spec.luau
Normal file
107
packages/charm/src/__tests__/peek.spec.luau
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
local Charm = require("../")
|
||||||
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local effect = Charm.effect
|
||||||
|
local onCleanup = Charm.onCleanup
|
||||||
|
local peek = Charm.peek
|
||||||
|
local test = suite.test
|
||||||
|
|
||||||
|
test("should not run detached", function()
|
||||||
|
local a = atom(1)
|
||||||
|
local spyCalls = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
a()
|
||||||
|
peek(function()
|
||||||
|
onCleanup(function()
|
||||||
|
spyCalls += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(spyCalls == 0, "should not be called yet")
|
||||||
|
a(2)
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
a(3)
|
||||||
|
assert(spyCalls == 2, "should be called twice")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should block tracking inside effects", function()
|
||||||
|
local a = atom(1)
|
||||||
|
local b = atom(2)
|
||||||
|
local spyCalls = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
peek(function()
|
||||||
|
spyCalls += 1
|
||||||
|
return a() + b()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
|
||||||
|
a(10)
|
||||||
|
b(20)
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should block tracking even when run inside effect run inside peek", function()
|
||||||
|
local s = atom(1)
|
||||||
|
local spyCalls = 0
|
||||||
|
|
||||||
|
peek(function()
|
||||||
|
effect(function()
|
||||||
|
peek(function()
|
||||||
|
spyCalls += 1
|
||||||
|
return s()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
|
||||||
|
s(2)
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should not cause atom assignments throw", function()
|
||||||
|
local a = atom(1)
|
||||||
|
local aChangedTime = atom(0)
|
||||||
|
|
||||||
|
local dispose = effect(function()
|
||||||
|
a()
|
||||||
|
peek(function()
|
||||||
|
aChangedTime(aChangedTime() + 1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a(2)
|
||||||
|
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
||||||
|
a(3)
|
||||||
|
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
||||||
|
|
||||||
|
dispose()
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should block tracking inside computed atoms", function()
|
||||||
|
local a = atom(1)
|
||||||
|
local b = atom(2)
|
||||||
|
local spyCalls = 0
|
||||||
|
local c = computed(function()
|
||||||
|
return peek(function()
|
||||||
|
spyCalls += 1
|
||||||
|
return a() + b()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(spyCalls == 0, "should not be called yet")
|
||||||
|
assert(c() == 3, "c should be 3")
|
||||||
|
a(10)
|
||||||
|
c()
|
||||||
|
b(20)
|
||||||
|
c()
|
||||||
|
assert(spyCalls == 1, "should be called once")
|
||||||
|
assert(c() == 3, "c should be 3")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -24,22 +24,20 @@ suite.describe("core", function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
suite.describe("charm", function()
|
suite.describe("listen", function()
|
||||||
suite.describe("listen", function()
|
require("./listen.spec")
|
||||||
require("./listen.spec")
|
end)
|
||||||
end)
|
suite.describe("mapped", function()
|
||||||
suite.describe("mapped", function()
|
require("./mapped.spec")
|
||||||
require("./mapped.spec")
|
end)
|
||||||
end)
|
suite.describe("observe", function()
|
||||||
suite.describe("observe", function()
|
require("./observe.spec")
|
||||||
require("./observe.spec")
|
end)
|
||||||
end)
|
suite.describe("subscribe", function()
|
||||||
suite.describe("subscribe", function()
|
require("./subscribe.spec")
|
||||||
require("./subscribe.spec")
|
end)
|
||||||
end)
|
suite.describe("untracked", function()
|
||||||
suite.describe("untracked", function()
|
require("./untracked.spec")
|
||||||
require("./untracked.spec")
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,29 @@ local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local untracked = Charm.untracked
|
local untracked = Charm.untracked
|
||||||
|
local onCleanup = Charm.onCleanup
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
|
test("should run detached", function()
|
||||||
|
local a = atom(1)
|
||||||
|
local spyCalls = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
a()
|
||||||
|
untracked(function()
|
||||||
|
onCleanup(function()
|
||||||
|
spyCalls += 1
|
||||||
|
end, true)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(spyCalls == 0, "should not be called")
|
||||||
|
a(2)
|
||||||
|
assert(spyCalls == 0, "rerun should not call cleanup")
|
||||||
|
a(3)
|
||||||
|
assert(spyCalls == 0, "rerun should not call cleanup")
|
||||||
|
end)
|
||||||
|
|
||||||
test("should block tracking inside effects", function()
|
test("should block tracking inside effects", function()
|
||||||
local a = atom(1)
|
local a = atom(1)
|
||||||
local b = atom(2)
|
local b = atom(2)
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,4 @@
|
||||||
--[[
|
local config = require("./flags")
|
||||||
alien-signals 2.0.5 implementation adopted from
|
|
||||||
https://github.com/Nicell/alien-signals-luau
|
|
||||||
https://github.com/stackblitz/alien-signals
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local strict = require("./strict")
|
|
||||||
local system = require("./system")
|
local system = require("./system")
|
||||||
local utils = require("./utils")
|
local utils = require("./utils")
|
||||||
|
|
||||||
|
|
@ -103,15 +75,15 @@ local function setCurrentScope(scope: Effect | EffectScope?)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setCurrentTracking(tracking: boolean): boolean
|
local function setCurrentTracking(tracking: boolean): boolean
|
||||||
local prevTracking = activeSubTracking
|
local prevSubTracking = activeSubTracking
|
||||||
activeSubTracking = tracking
|
activeSubTracking = tracking
|
||||||
return prevTracking
|
return prevSubTracking
|
||||||
end
|
end
|
||||||
|
|
||||||
local function runCleanups(effect: EffectScope)
|
local function runCleanups(effect: EffectScope)
|
||||||
for _, cleanup in effect.cleanups do
|
for _, cleanup in effect.cleanups do
|
||||||
local prev = setCurrentSub(nil)
|
local prev = setCurrentSub(nil)
|
||||||
local success, result: unknown = (if strict.enabled then noYield else pcall)(cleanup)
|
local success, result: unknown = (if config.strict then noYield else pcall)(cleanup)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
@ -136,7 +108,7 @@ local function run(effect: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||||
local prev = setCurrentSub(effect)
|
local prev = setCurrentSub(effect)
|
||||||
local prevTracking = setCurrentTracking(true)
|
local prevTracking = setCurrentTracking(true)
|
||||||
startTracking(effect)
|
startTracking(effect)
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn)
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)((effect :: Effect).fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
endTracking(effect)
|
endTracking(effect)
|
||||||
setCurrentTracking(prevTracking)
|
setCurrentTracking(prevTracking)
|
||||||
|
|
@ -194,7 +166,7 @@ local function updateComputed(computed: Computed): boolean
|
||||||
local prevTracking = setCurrentTracking(true)
|
local prevTracking = setCurrentTracking(true)
|
||||||
startTracking(computed)
|
startTracking(computed)
|
||||||
local oldValue = computed.value
|
local oldValue = computed.value
|
||||||
local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue)
|
local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue)
|
||||||
setCurrentSub(prevSub)
|
setCurrentSub(prevSub)
|
||||||
endTracking(computed)
|
endTracking(computed)
|
||||||
setCurrentTracking(prevTracking)
|
setCurrentTracking(prevTracking)
|
||||||
|
|
@ -262,7 +234,7 @@ local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
|
||||||
local newValue = ...
|
local newValue = ...
|
||||||
if type(newValue) == "function" then
|
if type(newValue) == "function" then
|
||||||
local prev = setCurrentSub(nil)
|
local prev = setCurrentSub(nil)
|
||||||
local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue)
|
local success, result = (if config.strict then noYield else pcall)(newValue, oldValue)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
@ -380,7 +352,7 @@ local function effect(fn: () -> ...(() -> ())?): () -> ()
|
||||||
end
|
end
|
||||||
local prev = setCurrentSub(self)
|
local prev = setCurrentSub(self)
|
||||||
local prevTracking = setCurrentTracking(true)
|
local prevTracking = setCurrentTracking(true)
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
setCurrentTracking(prevTracking)
|
setCurrentTracking(prevTracking)
|
||||||
if not success then
|
if not success then
|
||||||
|
|
@ -401,7 +373,7 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () ->
|
||||||
end
|
end
|
||||||
local prevSub = setCurrentSub(nil)
|
local prevSub = setCurrentSub(nil)
|
||||||
local prevScope = setCurrentScope(self)
|
local prevScope = setCurrentScope(self)
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
|
||||||
setCurrentScope(prevScope)
|
setCurrentScope(prevScope)
|
||||||
setCurrentSub(prevSub)
|
setCurrentSub(prevSub)
|
||||||
if not success then
|
if not success then
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
-- Enable strict mode in O2
|
-- Enable strict mode in O2
|
||||||
enabled = inlineTest() == "inlineTest",
|
strict = inlineTest() == "inlineTest",
|
||||||
}
|
}
|
||||||
69
packages/charm/src/index.d.ts
vendored
69
packages/charm/src/index.d.ts
vendored
|
|
@ -1 +1,68 @@
|
||||||
export {};
|
type Key = string | number | symbol;
|
||||||
|
type Cleanup = () => void;
|
||||||
|
|
||||||
|
export interface Atom<T> {
|
||||||
|
(): T;
|
||||||
|
(value: T | ((value: T) => T)): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Selector<T> = () => T;
|
||||||
|
|
||||||
|
export type Equals<T> = (current: T, incoming: T) => boolean;
|
||||||
|
|
||||||
|
export const flags: {
|
||||||
|
strict: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
||||||
|
// Overload for undefined initial value
|
||||||
|
export function atom<T>(): Atom<T | undefined>;
|
||||||
|
|
||||||
|
export function computed<T>(getter: (previousValue?: T) => T, equals?: Equals<T>): () => T;
|
||||||
|
|
||||||
|
export function effect(callback: () => Cleanup | void): Cleanup;
|
||||||
|
|
||||||
|
export function effectScope(callback: () => Cleanup | void, detached?: boolean): Cleanup;
|
||||||
|
|
||||||
|
export function listen<T>(
|
||||||
|
callback: () => T,
|
||||||
|
listener: (value: T, previousValue: T | undefined, dispose: Cleanup) => void,
|
||||||
|
): Cleanup;
|
||||||
|
|
||||||
|
export function subscribe<T>(callback: () => T, listener: (value: T, previousValue: T) => void): Cleanup;
|
||||||
|
|
||||||
|
export function mapped<VI, KI, VO, KO = KI>(
|
||||||
|
callback: () => Map<KI, VI> | ReadonlyMap<KI, VI>,
|
||||||
|
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
||||||
|
): () => ReadonlyMap<KO, VO>;
|
||||||
|
// Overload for arrays
|
||||||
|
export function mapped<VI, VO, K extends Key = number>(
|
||||||
|
callback: () => readonly VI[],
|
||||||
|
mapper: (value: VI, key: number) => LuaTuple<[VO, K]> | VO,
|
||||||
|
): () => K extends number ? readonly VO[] : { readonly [P in K]: VO };
|
||||||
|
// Overload for objects
|
||||||
|
export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
|
||||||
|
callback: () => { readonly [K in KI]: VI },
|
||||||
|
mapper: (value: VI, key: KI) => LuaTuple<[VO, KO]> | VO,
|
||||||
|
): () => { readonly [K in KO]: VO };
|
||||||
|
|
||||||
|
export function observe<V, K = number>(
|
||||||
|
callback: () => Map<K, V> | ReadonlyMap<K, V> | readonly V[],
|
||||||
|
observer: (value: V, key: K) => Cleanup | void,
|
||||||
|
): Cleanup;
|
||||||
|
// Overload for objects
|
||||||
|
export function observe<V, K extends Key>(
|
||||||
|
callback: () => { readonly [K in Key]: V },
|
||||||
|
observer: (value: V, key: K) => Cleanup | void,
|
||||||
|
): Cleanup;
|
||||||
|
|
||||||
|
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||||
|
|
||||||
|
export function batched<Args extends any[], Result>(
|
||||||
|
callback: (...args: Args) => Result,
|
||||||
|
...args: Args
|
||||||
|
): (...args: Args) => Result;
|
||||||
|
|
||||||
|
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
|
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
local batched = require("@self/batched")
|
local batched = require("@self/batched")
|
||||||
local core = require("@self/core")
|
local core = require("@self/core")
|
||||||
|
local flags = require("@self/flags")
|
||||||
local listen = require("@self/listen")
|
local listen = require("@self/listen")
|
||||||
local mapped = require("@self/mapped")
|
local mapped = require("@self/mapped")
|
||||||
local observe = require("@self/observe")
|
local observe = require("@self/observe")
|
||||||
local strict = require("@self/strict")
|
local peek = require("@self/peek")
|
||||||
local subscribe = require("@self/subscribe")
|
local subscribe = require("@self/subscribe")
|
||||||
local types = require("@self/types")
|
local types = require("@self/types")
|
||||||
local untracked = require("@self/untracked")
|
local untracked = require("@self/untracked")
|
||||||
|
|
@ -11,24 +12,19 @@ local untracked = require("@self/untracked")
|
||||||
export type Atom<T = any> = types.Atom<T>
|
export type Atom<T = any> = types.Atom<T>
|
||||||
export type Selector<T = any> = types.Selector<T>
|
export type Selector<T = any> = types.Selector<T>
|
||||||
|
|
||||||
local atom = core.atom
|
|
||||||
local computed = core.computed
|
|
||||||
local effect = core.effect
|
|
||||||
local effectScope = core.effectScope
|
|
||||||
local onCleanup = core.onCleanup
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
core = core,
|
core = core,
|
||||||
strict = strict,
|
flags = flags,
|
||||||
atom = atom,
|
atom = core.atom,
|
||||||
|
computed = core.computed,
|
||||||
|
effect = core.effect,
|
||||||
|
effectScope = core.effectScope,
|
||||||
|
onCleanup = core.onCleanup,
|
||||||
batched = batched,
|
batched = batched,
|
||||||
computed = computed,
|
|
||||||
effect = effect,
|
|
||||||
effectScope = effectScope,
|
|
||||||
listen = listen,
|
listen = listen,
|
||||||
mapped = mapped,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
onCleanup = onCleanup,
|
peek = peek,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local untracked = require("./untracked")
|
local peek = require("./peek")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -19,7 +19,7 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
untracked(listener, current, previous, dispose)
|
peek(listener, current, previous, dispose)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposed then
|
if disposed then
|
||||||
|
|
|
||||||
18
packages/charm/src/peek.luau
Normal file
18
packages/charm/src/peek.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
local core = require("./core")
|
||||||
|
|
||||||
|
local setCurrentTracking = core.setCurrentTracking
|
||||||
|
|
||||||
|
local function handlePeek(prevTracking: boolean, ok: boolean, ...)
|
||||||
|
setCurrentTracking(prevTracking)
|
||||||
|
if ok then
|
||||||
|
return ...
|
||||||
|
else
|
||||||
|
error((...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
return handlePeek(setCurrentTracking(false), pcall(callback, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
return peek
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local untracked = require("./untracked")
|
local peek = require("./peek")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -18,7 +18,7 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
|
||||||
if mounted then
|
if mounted then
|
||||||
mounted = false
|
mounted = false
|
||||||
else
|
else
|
||||||
untracked(listener, current, previous)
|
peek(listener, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
|
|
||||||
local setCurrentTracking = core.setCurrentTracking
|
local setCurrentSub = core.setCurrentSub
|
||||||
|
|
||||||
local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
local function handleUntracked(prevSub: core.ReactiveNode?, ok: boolean, ...)
|
||||||
setCurrentTracking(prevTracking)
|
setCurrentSub(prevSub)
|
||||||
if ok then
|
if ok then
|
||||||
return ...
|
return ...
|
||||||
else
|
else
|
||||||
|
|
@ -12,7 +12,7 @@ local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
|
return handleUntracked(setCurrentSub(nil), pcall(callback, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
return untracked
|
return untracked
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "charm",
|
|
||||||
"tree": {
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local startBatch = core.startBatch
|
|
||||||
local endBatch = core.endBatch
|
|
||||||
|
|
||||||
local function handleBatched(ok: boolean, ...)
|
|
||||||
endBatch()
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
startBatch()
|
|
||||||
return handleBatched(pcall(callback, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return batched
|
|
||||||
|
|
@ -1,445 +0,0 @@
|
||||||
--[[
|
|
||||||
alien-signals 2.0.5 implementation adopted from
|
|
||||||
https://github.com/Nicell/alien-signals-luau
|
|
||||||
https://github.com/stackblitz/alien-signals
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local strict = require("./strict")
|
|
||||||
local system = require("./system")
|
|
||||||
local utils = require("./utils")
|
|
||||||
|
|
||||||
export type ReactiveFlags = system.ReactiveFlags
|
|
||||||
export type ReactiveNode = system.ReactiveNode
|
|
||||||
export type Link = system.Link
|
|
||||||
|
|
||||||
local ReactiveFlags = system.ReactiveFlags
|
|
||||||
local createReactiveSystem = system.createReactiveSystem
|
|
||||||
local noYield = utils.noYield
|
|
||||||
|
|
||||||
local NONE = 0b0000000
|
|
||||||
local MUTABLE = 0b0000001
|
|
||||||
local WATCHING = 0b0000010
|
|
||||||
local DIRTY = 0b0010000
|
|
||||||
local PENDING = 0b0100000
|
|
||||||
local QUEUED = 0b1000000
|
|
||||||
|
|
||||||
type EffectScope = ReactiveNode & {
|
|
||||||
cleanups: { () -> () },
|
|
||||||
}
|
|
||||||
|
|
||||||
type Effect = ReactiveNode & {
|
|
||||||
fn: () -> ...(() -> ())?,
|
|
||||||
cleanups: { () -> () },
|
|
||||||
}
|
|
||||||
|
|
||||||
type Computed<T = any> = ReactiveNode & {
|
|
||||||
value: T?,
|
|
||||||
getter: (previousValue: T?) -> T,
|
|
||||||
equals: ((T, T) -> boolean)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Atom<T = any> = ReactiveNode & {
|
|
||||||
previousValue: T,
|
|
||||||
value: T,
|
|
||||||
equals: ((T, T) -> boolean)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
|
||||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
|
||||||
local propagate: (link: Link) -> ()
|
|
||||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
|
||||||
local endTracking: (sub: ReactiveNode) -> ()
|
|
||||||
local startTracking: (sub: ReactiveNode) -> ()
|
|
||||||
local shallowPropagate: (link: Link) -> ()
|
|
||||||
|
|
||||||
local batchDepth = 0
|
|
||||||
local queuedEffects: { Effect | EffectScope? } = {}
|
|
||||||
local queuedEffectsLength = 0
|
|
||||||
local notifyIndex = 0
|
|
||||||
local activeSubTracking = true
|
|
||||||
local activeSub: ReactiveNode?
|
|
||||||
local activeScope: Effect | EffectScope?
|
|
||||||
|
|
||||||
local function getCurrentSub(): ReactiveNode?
|
|
||||||
return activeSub
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setCurrentSub(sub: ReactiveNode?)
|
|
||||||
local prevSub = activeSub
|
|
||||||
activeSub = sub
|
|
||||||
return prevSub
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getCurrentScope(): Effect | EffectScope?
|
|
||||||
return activeScope
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setCurrentScope(scope: Effect | EffectScope?)
|
|
||||||
local prevScope = activeScope
|
|
||||||
activeScope = scope
|
|
||||||
return prevScope
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setCurrentTracking(tracking: boolean): boolean
|
|
||||||
local prevTracking = activeSubTracking
|
|
||||||
activeSubTracking = tracking
|
|
||||||
return prevTracking
|
|
||||||
end
|
|
||||||
|
|
||||||
local function runCleanups(effect: EffectScope)
|
|
||||||
for _, cleanup in effect.cleanups do
|
|
||||||
local prev = setCurrentSub(nil)
|
|
||||||
local success, result: unknown = (if strict.enabled then noYield else pcall)(cleanup)
|
|
||||||
setCurrentSub(prev)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(effect.cleanups)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
|
||||||
if activeSub then
|
|
||||||
table.insert((activeSub :: Effect).cleanups, cleanup)
|
|
||||||
elseif activeScope then
|
|
||||||
table.insert(activeScope.cleanups, cleanup)
|
|
||||||
elseif not failSilently then
|
|
||||||
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run(effect: Effect | EffectScope, flags: ReactiveFlags): ()
|
|
||||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
|
||||||
runCleanups(effect)
|
|
||||||
local prev = setCurrentSub(effect)
|
|
||||||
local prevTracking = setCurrentTracking(true)
|
|
||||||
startTracking(effect)
|
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn)
|
|
||||||
setCurrentSub(prev)
|
|
||||||
endTracking(effect)
|
|
||||||
setCurrentTracking(prevTracking)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
elseif result then
|
|
||||||
table.insert(effect.cleanups, result)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
elseif bit32.btest(flags, PENDING) then
|
|
||||||
(effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
|
|
||||||
end
|
|
||||||
local link = effect.deps
|
|
||||||
while link do
|
|
||||||
local dep = link.dep
|
|
||||||
local depFlags = dep.flags
|
|
||||||
if bit32.btest(depFlags, QUEUED) then
|
|
||||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
|
||||||
dep.flags = depFlags
|
|
||||||
run(dep :: Effect, depFlags)
|
|
||||||
end
|
|
||||||
link = link.nextDep
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function flush(): ()
|
|
||||||
while notifyIndex < queuedEffectsLength do
|
|
||||||
notifyIndex += 1
|
|
||||||
local effect = queuedEffects[notifyIndex] :: Effect
|
|
||||||
queuedEffects[notifyIndex] = nil
|
|
||||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
|
||||||
run(effect, effect.flags)
|
|
||||||
end
|
|
||||||
notifyIndex = 0
|
|
||||||
queuedEffectsLength = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getBatchDepth(): number
|
|
||||||
return batchDepth
|
|
||||||
end
|
|
||||||
|
|
||||||
local function startBatch()
|
|
||||||
batchDepth += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function endBatch()
|
|
||||||
batchDepth -= 1
|
|
||||||
if batchDepth == 0 then
|
|
||||||
flush()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function updateComputed(computed: Computed): boolean
|
|
||||||
local prevSub = setCurrentSub(computed)
|
|
||||||
local prevTracking = setCurrentTracking(true)
|
|
||||||
startTracking(computed)
|
|
||||||
local oldValue = computed.value
|
|
||||||
local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue)
|
|
||||||
setCurrentSub(prevSub)
|
|
||||||
endTracking(computed)
|
|
||||||
setCurrentTracking(prevTracking)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
local equals = computed.equals
|
|
||||||
if if equals then not equals(oldValue, result) else oldValue ~= result then
|
|
||||||
computed.value = result
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function updateAtom(atom: Atom, value: any): boolean
|
|
||||||
atom.flags = MUTABLE
|
|
||||||
if atom.previousValue ~= value then
|
|
||||||
atom.previousValue = value
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function readAtom<T>(atom: Atom<T>): T
|
|
||||||
local value = atom.value
|
|
||||||
if bit32.btest(atom.flags, DIRTY) then
|
|
||||||
if updateAtom(atom, value) then
|
|
||||||
local subs = atom.subs
|
|
||||||
if subs then
|
|
||||||
shallowPropagate(subs)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if activeSub and activeSubTracking then
|
|
||||||
link(atom, activeSub)
|
|
||||||
end
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
|
|
||||||
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
|
|
||||||
local equals = atom.equals
|
|
||||||
if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then
|
|
||||||
atom.value = newValue
|
|
||||||
atom.flags = bit32.bor(MUTABLE, DIRTY)
|
|
||||||
local subs = atom.subs
|
|
||||||
if subs then
|
|
||||||
propagate(subs)
|
|
||||||
if batchDepth == 0 then
|
|
||||||
flush()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return newValue
|
|
||||||
end
|
|
||||||
return oldValue
|
|
||||||
end
|
|
||||||
|
|
||||||
local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
|
|
||||||
return function(...)
|
|
||||||
if select("#", ...) == 0 then
|
|
||||||
return readAtom(atom)
|
|
||||||
end
|
|
||||||
local oldValue = atom.value
|
|
||||||
local newValue = ...
|
|
||||||
if type(newValue) == "function" then
|
|
||||||
local prev = setCurrentSub(nil)
|
|
||||||
local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue)
|
|
||||||
setCurrentSub(prev)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
newValue = result
|
|
||||||
end
|
|
||||||
return writeAtom(atom, oldValue, newValue :: T)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function computedGetter<T>(computed: Computed<T>): () -> T
|
|
||||||
return function()
|
|
||||||
local flags = computed.flags
|
|
||||||
if
|
|
||||||
bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
|
|
||||||
then
|
|
||||||
if updateComputed(computed) then
|
|
||||||
local subs = computed.subs
|
|
||||||
if subs then
|
|
||||||
shallowPropagate(subs)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif bit32.btest(flags, PENDING) then
|
|
||||||
computed.flags = bit32.band(flags, bit32.bnot(PENDING))
|
|
||||||
end
|
|
||||||
if activeSub then
|
|
||||||
if activeSubTracking then
|
|
||||||
link(computed, activeSub)
|
|
||||||
end
|
|
||||||
elseif activeScope then
|
|
||||||
link(computed, activeScope)
|
|
||||||
end
|
|
||||||
return computed.value :: T
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function effectStop(self: Effect | EffectScope): () -> ()
|
|
||||||
return function()
|
|
||||||
local dep = self.deps
|
|
||||||
while dep do
|
|
||||||
dep = unlink(dep, self)
|
|
||||||
end
|
|
||||||
local sub = self.subs
|
|
||||||
if sub then
|
|
||||||
unlink(sub)
|
|
||||||
end
|
|
||||||
(self :: Effect).flags = NONE
|
|
||||||
runCleanups(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update(atom: Atom | Computed): boolean
|
|
||||||
if (atom :: Computed).getter then
|
|
||||||
return updateComputed(atom :: Computed)
|
|
||||||
else
|
|
||||||
return updateAtom(atom :: Atom, atom.value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function notify(effect: Effect | EffectScope)
|
|
||||||
local flags = effect.flags
|
|
||||||
if not bit32.btest(flags, QUEUED) then
|
|
||||||
(effect :: Effect).flags = bit32.bor(flags, QUEUED)
|
|
||||||
local subs = effect.subs
|
|
||||||
if subs then
|
|
||||||
notify(subs.sub :: Effect)
|
|
||||||
else
|
|
||||||
queuedEffectsLength += 1
|
|
||||||
queuedEffects[queuedEffectsLength] = effect
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function unwatched(node: Atom | Computed | Effect | EffectScope)
|
|
||||||
if (node :: Computed).getter then
|
|
||||||
local toRemove = node.deps
|
|
||||||
if toRemove then
|
|
||||||
(node :: Computed).flags = bit32.bor(MUTABLE, DIRTY)
|
|
||||||
repeat
|
|
||||||
toRemove = unlink(toRemove, node)
|
|
||||||
until not toRemove
|
|
||||||
end
|
|
||||||
elseif (node :: Effect).cleanups then
|
|
||||||
effectStop(node :: Effect)()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function atom<T>(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
|
||||||
return atomGetterSetter({
|
|
||||||
previousValue = initialValue,
|
|
||||||
value = initialValue,
|
|
||||||
flags = MUTABLE,
|
|
||||||
equals = equals,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T, equals: ((T, T) -> boolean)?): () -> T
|
|
||||||
return computedGetter({
|
|
||||||
flags = bit32.bor(MUTABLE, DIRTY),
|
|
||||||
getter = getter,
|
|
||||||
equals = equals,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function effect(fn: () -> ...(() -> ())?): () -> ()
|
|
||||||
local self: Effect = {
|
|
||||||
fn = fn,
|
|
||||||
flags = WATCHING,
|
|
||||||
cleanups = {},
|
|
||||||
}
|
|
||||||
if activeSub then
|
|
||||||
link(self, activeSub)
|
|
||||||
elseif activeScope then
|
|
||||||
link(self, activeScope)
|
|
||||||
end
|
|
||||||
local prev = setCurrentSub(self)
|
|
||||||
local prevTracking = setCurrentTracking(true)
|
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
|
||||||
setCurrentSub(prev)
|
|
||||||
setCurrentTracking(prevTracking)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
elseif result then
|
|
||||||
table.insert(self.cleanups, result)
|
|
||||||
end
|
|
||||||
return effectStop(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
|
||||||
local self: EffectScope = {
|
|
||||||
flags = NONE,
|
|
||||||
cleanups = {},
|
|
||||||
}
|
|
||||||
if activeScope and not detached then
|
|
||||||
link(self, activeScope)
|
|
||||||
end
|
|
||||||
local prevSub = setCurrentSub(nil)
|
|
||||||
local prevScope = setCurrentScope(self)
|
|
||||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
|
||||||
setCurrentScope(prevScope)
|
|
||||||
setCurrentSub(prevSub)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
elseif result then
|
|
||||||
table.insert(self.cleanups, result)
|
|
||||||
end
|
|
||||||
return effectStop(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local reactiveSystem = createReactiveSystem({
|
|
||||||
update = update :: (ReactiveNode) -> boolean,
|
|
||||||
notify = notify :: (ReactiveNode) -> (),
|
|
||||||
unwatched = unwatched :: (ReactiveNode) -> (),
|
|
||||||
})
|
|
||||||
|
|
||||||
link = reactiveSystem.link
|
|
||||||
unlink = reactiveSystem.unlink
|
|
||||||
propagate = reactiveSystem.propagate
|
|
||||||
checkDirty = reactiveSystem.checkDirty
|
|
||||||
endTracking = reactiveSystem.endTracking
|
|
||||||
startTracking = reactiveSystem.startTracking
|
|
||||||
shallowPropagate = reactiveSystem.shallowPropagate
|
|
||||||
|
|
||||||
return {
|
|
||||||
ReactiveFlags = ReactiveFlags,
|
|
||||||
createReactiveSystem = createReactiveSystem,
|
|
||||||
getCurrentSub = getCurrentSub,
|
|
||||||
setCurrentSub = setCurrentSub,
|
|
||||||
getCurrentScope = getCurrentScope,
|
|
||||||
setCurrentScope = setCurrentScope,
|
|
||||||
setCurrentTracking = setCurrentTracking,
|
|
||||||
onCleanup = onCleanup,
|
|
||||||
getBatchDepth = getBatchDepth,
|
|
||||||
startBatch = startBatch,
|
|
||||||
endBatch = endBatch,
|
|
||||||
atom = atom,
|
|
||||||
computed = computed,
|
|
||||||
effect = effect,
|
|
||||||
effectScope = effectScope,
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
local batched = require("@self/batched")
|
|
||||||
local core = require("@self/core")
|
|
||||||
local listen = require("@self/listen")
|
|
||||||
local mapped = require("@self/mapped")
|
|
||||||
local observe = require("@self/observe")
|
|
||||||
local strict = require("@self/strict")
|
|
||||||
local subscribe = require("@self/subscribe")
|
|
||||||
local types = require("@self/types")
|
|
||||||
local untracked = require("@self/untracked")
|
|
||||||
|
|
||||||
export type Atom<T = any> = types.Atom<T>
|
|
||||||
export type Selector<T = any> = types.Selector<T>
|
|
||||||
|
|
||||||
local atom = core.atom
|
|
||||||
local computed = core.computed
|
|
||||||
local effect = core.effect
|
|
||||||
local effectScope = core.effectScope
|
|
||||||
local onCleanup = core.onCleanup
|
|
||||||
|
|
||||||
return {
|
|
||||||
core = core,
|
|
||||||
strict = strict,
|
|
||||||
atom = atom,
|
|
||||||
batched = batched,
|
|
||||||
computed = computed,
|
|
||||||
effect = effect,
|
|
||||||
effectScope = effectScope,
|
|
||||||
listen = listen,
|
|
||||||
mapped = mapped,
|
|
||||||
observe = observe,
|
|
||||||
onCleanup = onCleanup,
|
|
||||||
subscribe = subscribe,
|
|
||||||
untracked = untracked,
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
local untracked = require("./untracked")
|
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local effect = core.effect
|
|
||||||
|
|
||||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
|
||||||
local tracked = computed(function()
|
|
||||||
return selector()
|
|
||||||
end)
|
|
||||||
|
|
||||||
local current: T
|
|
||||||
local disposed = false
|
|
||||||
|
|
||||||
local function dispose()
|
|
||||||
disposed = true
|
|
||||||
end
|
|
||||||
|
|
||||||
dispose = effect(function()
|
|
||||||
local previous = current
|
|
||||||
current = tracked()
|
|
||||||
untracked(listener, current, previous, dispose)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if disposed then
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
|
|
||||||
return dispose
|
|
||||||
end
|
|
||||||
|
|
||||||
return listen
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local setCurrentSub = core.setCurrentSub
|
|
||||||
|
|
||||||
type Mapped =
|
|
||||||
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
|
||||||
& (<K, VI, VO>(selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
|
|
||||||
|
|
||||||
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
|
||||||
local currentData: { [KI]: VI } = {}
|
|
||||||
local keyCache: { [KI]: KO } = {}
|
|
||||||
|
|
||||||
local function map(value: VI, key: KI): (VO, KO)
|
|
||||||
local currentSub = setCurrentSub(nil)
|
|
||||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
|
||||||
setCurrentSub(currentSub)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
error(`mapped() transform function failed: {valueOut}`)
|
|
||||||
end
|
|
||||||
|
|
||||||
return valueOut, if keyOut == nil then key else keyOut :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
return computed(function(previousOutput)
|
|
||||||
local currentOutput: { [KO]: VO } = previousOutput or {}
|
|
||||||
local previousData = currentData
|
|
||||||
currentData = selector()
|
|
||||||
|
|
||||||
for key in previousData do
|
|
||||||
if currentData[key] ~= nil then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
local cachedKey = keyCache[key]
|
|
||||||
if cachedKey then
|
|
||||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
|
||||||
currentOutput[cachedKey] = nil
|
|
||||||
keyCache[key] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, value in currentData do
|
|
||||||
if value == previousData[key] then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
local valueOut, keyOut = map(value, key)
|
|
||||||
if currentOutput[keyOut] ~= valueOut then
|
|
||||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
|
||||||
currentOutput[keyOut] = valueOut
|
|
||||||
keyCache[key] = keyOut
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return currentOutput
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return mapped :: Mapped
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local effect = core.effect
|
|
||||||
local effectScope = core.effectScope
|
|
||||||
local onCleanup = core.onCleanup
|
|
||||||
|
|
||||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
|
||||||
local scopes: { [K]: () -> () } = {}
|
|
||||||
local currentItems: { [K]: V } = {}
|
|
||||||
|
|
||||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
|
||||||
for key in previousItems do
|
|
||||||
local dispose = scopes[key]
|
|
||||||
if currentItems[key] == nil and dispose then
|
|
||||||
dispose()
|
|
||||||
scopes[key] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, value in currentItems do
|
|
||||||
if previousItems[key] == nil then
|
|
||||||
scopes[key] = effectScope(function()
|
|
||||||
return observer(value, key)
|
|
||||||
end, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function disposeScopes()
|
|
||||||
for _, dispose in scopes do
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
table.clear(scopes)
|
|
||||||
end
|
|
||||||
|
|
||||||
local disposeEffect = effect(function()
|
|
||||||
local previousItems = currentItems
|
|
||||||
currentItems = selector()
|
|
||||||
if currentItems ~= previousItems then
|
|
||||||
updateScopes(currentItems, previousItems)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
onCleanup(disposeScopes, true)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
disposeEffect()
|
|
||||||
disposeScopes()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return observe
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
local function inlineTest(): string
|
|
||||||
return debug.info(1, "n")
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
-- Enable strict mode in O2
|
|
||||||
enabled = inlineTest() == "inlineTest",
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
local untracked = require("./untracked")
|
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local effect = core.effect
|
|
||||||
|
|
||||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
|
||||||
local tracked = computed(function()
|
|
||||||
return selector()
|
|
||||||
end)
|
|
||||||
|
|
||||||
local current: T
|
|
||||||
local mounted = true
|
|
||||||
|
|
||||||
return effect(function()
|
|
||||||
local previous = current
|
|
||||||
current = tracked()
|
|
||||||
if mounted then
|
|
||||||
mounted = false
|
|
||||||
else
|
|
||||||
untracked(listener, current, previous)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return subscribe
|
|
||||||
|
|
@ -1,357 +0,0 @@
|
||||||
--[[
|
|
||||||
alien-signals 2.0.1 implementation adopted from
|
|
||||||
https://github.com/Nicell/alien-signals-luau
|
|
||||||
https://github.com/stackblitz/alien-signals
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
]]
|
|
||||||
|
|
||||||
export type ReactiveNode = {
|
|
||||||
deps: Link?,
|
|
||||||
depsTail: Link?,
|
|
||||||
subs: Link?,
|
|
||||||
subsTail: Link?,
|
|
||||||
flags: ReactiveFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Link = {
|
|
||||||
dep: ReactiveNode,
|
|
||||||
sub: ReactiveNode,
|
|
||||||
prevSub: Link?,
|
|
||||||
nextSub: Link?,
|
|
||||||
prevDep: Link?,
|
|
||||||
nextDep: Link?,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stack<T> = {
|
|
||||||
value: T,
|
|
||||||
prev: Stack<T>?,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ReactiveFlags = number
|
|
||||||
|
|
||||||
local NONE = 0b0000000
|
|
||||||
local MUTABLE = 0b0000001
|
|
||||||
local WATCHING = 0b0000010
|
|
||||||
local RECURSED_CHECK = 0b0000100
|
|
||||||
local RECURSED = 0b0001000
|
|
||||||
local DIRTY = 0b0010000
|
|
||||||
local PENDING = 0b0100000
|
|
||||||
|
|
||||||
local ReactiveFlags = {
|
|
||||||
None = NONE,
|
|
||||||
Mutable = MUTABLE,
|
|
||||||
Watching = WATCHING,
|
|
||||||
RecursedCheck = RECURSED_CHECK,
|
|
||||||
Recursed = RECURSED,
|
|
||||||
Dirty = DIRTY,
|
|
||||||
Pending = PENDING,
|
|
||||||
}
|
|
||||||
|
|
||||||
local function createReactiveSystem(props: {
|
|
||||||
update: (sub: ReactiveNode) -> boolean,
|
|
||||||
notify: (sub: ReactiveNode) -> (),
|
|
||||||
unwatched: (sub: ReactiveNode) -> (),
|
|
||||||
})
|
|
||||||
local update = props.update
|
|
||||||
local notify = props.notify
|
|
||||||
local unwatched = props.unwatched
|
|
||||||
|
|
||||||
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
|
||||||
local depsTail = sub.depsTail
|
|
||||||
if depsTail then
|
|
||||||
local link = sub.deps :: Link
|
|
||||||
repeat
|
|
||||||
if link == checkLink then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if link == depsTail then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
link = link.nextDep :: Link
|
|
||||||
until not link
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function link(dep: ReactiveNode, sub: ReactiveNode): ()
|
|
||||||
local prevDep = sub.depsTail
|
|
||||||
if prevDep and prevDep.dep == dep then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local nextDep: Link?
|
|
||||||
local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK)
|
|
||||||
if recursedCheck then
|
|
||||||
nextDep = if prevDep then prevDep.nextDep else sub.deps
|
|
||||||
if nextDep and nextDep.dep == dep then
|
|
||||||
sub.depsTail = nextDep
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local prevSub = dep.subsTail
|
|
||||||
if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local newLink: Link = {
|
|
||||||
dep = dep,
|
|
||||||
sub = sub,
|
|
||||||
prevDep = prevDep,
|
|
||||||
nextDep = nextDep,
|
|
||||||
prevSub = prevSub,
|
|
||||||
}
|
|
||||||
sub.depsTail = newLink
|
|
||||||
dep.subsTail = newLink
|
|
||||||
if nextDep then
|
|
||||||
nextDep.prevDep = newLink
|
|
||||||
end
|
|
||||||
if prevDep then
|
|
||||||
prevDep.nextDep = newLink
|
|
||||||
else
|
|
||||||
sub.deps = newLink
|
|
||||||
end
|
|
||||||
if prevSub then
|
|
||||||
prevSub.nextSub = newLink
|
|
||||||
else
|
|
||||||
dep.subs = newLink
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function unlink(link: Link, sub: ReactiveNode?): Link?
|
|
||||||
local sub = sub or link.sub
|
|
||||||
local dep = link.dep
|
|
||||||
local prevDep = link.prevDep
|
|
||||||
local nextDep = link.nextDep
|
|
||||||
local nextSub = link.nextSub
|
|
||||||
local prevSub = link.prevSub
|
|
||||||
if nextDep then
|
|
||||||
nextDep.prevDep = prevDep
|
|
||||||
else
|
|
||||||
sub.depsTail = prevDep
|
|
||||||
end
|
|
||||||
if prevDep then
|
|
||||||
prevDep.nextDep = nextDep
|
|
||||||
else
|
|
||||||
sub.deps = nextDep
|
|
||||||
end
|
|
||||||
if nextSub then
|
|
||||||
nextSub.prevSub = prevSub
|
|
||||||
else
|
|
||||||
dep.subsTail = prevSub
|
|
||||||
end
|
|
||||||
if prevSub then
|
|
||||||
prevSub.nextSub = nextSub
|
|
||||||
else
|
|
||||||
dep.subs = nextSub
|
|
||||||
if not nextSub then
|
|
||||||
unwatched(dep)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nextDep
|
|
||||||
end
|
|
||||||
|
|
||||||
local function propagate(link: Link): ()
|
|
||||||
local next = link.nextSub
|
|
||||||
local stack: Stack<Link?>?
|
|
||||||
|
|
||||||
repeat
|
|
||||||
local sub = link.sub
|
|
||||||
|
|
||||||
local flags = sub.flags
|
|
||||||
|
|
||||||
if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then
|
|
||||||
if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
|
|
||||||
sub.flags = bit32.bor(flags, PENDING)
|
|
||||||
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
|
|
||||||
flags = NONE
|
|
||||||
elseif not bit32.btest(flags, RECURSED_CHECK) then
|
|
||||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
|
||||||
elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then
|
|
||||||
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
|
|
||||||
flags = bit32.band(flags, MUTABLE)
|
|
||||||
else
|
|
||||||
-- selene: allow(if_same_then_else)
|
|
||||||
flags = NONE
|
|
||||||
end
|
|
||||||
|
|
||||||
if bit32.btest(flags, WATCHING) then
|
|
||||||
notify(sub)
|
|
||||||
end
|
|
||||||
|
|
||||||
if bit32.btest(flags, MUTABLE) then
|
|
||||||
local subSubs = sub.subs
|
|
||||||
if subSubs then
|
|
||||||
link = subSubs
|
|
||||||
if subSubs.nextSub then
|
|
||||||
stack = { value = next, prev = stack }
|
|
||||||
next = link.nextSub
|
|
||||||
end
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
link = next :: Link
|
|
||||||
|
|
||||||
if link then
|
|
||||||
next = link.nextSub
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local continueTop = false
|
|
||||||
|
|
||||||
while stack do
|
|
||||||
link = stack.value :: Link
|
|
||||||
stack = stack.prev
|
|
||||||
if link then
|
|
||||||
next = link.nextSub
|
|
||||||
continueTop = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if continueTop then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
break
|
|
||||||
until false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function startTracking(sub: ReactiveNode): ()
|
|
||||||
sub.depsTail = nil
|
|
||||||
sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function endTracking(sub: ReactiveNode): ()
|
|
||||||
local depsTail = sub.depsTail
|
|
||||||
local toRemove = if depsTail then depsTail.nextDep else sub.deps
|
|
||||||
while toRemove do
|
|
||||||
toRemove = unlink(toRemove, sub)
|
|
||||||
end
|
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(RECURSED_CHECK))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shallowPropagate(link: Link): ()
|
|
||||||
repeat
|
|
||||||
local sub = link.sub
|
|
||||||
local subFlags = sub.flags
|
|
||||||
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then
|
|
||||||
sub.flags = bit32.bor(subFlags, DIRTY)
|
|
||||||
if bit32.btest(subFlags, WATCHING) then
|
|
||||||
notify(sub)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
link = link.nextSub :: Link
|
|
||||||
until not link
|
|
||||||
end
|
|
||||||
|
|
||||||
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
|
||||||
local stack: Stack<Link>?
|
|
||||||
local checkDepth = 0
|
|
||||||
|
|
||||||
repeat
|
|
||||||
local dep = link.dep
|
|
||||||
local depFlags = dep.flags
|
|
||||||
|
|
||||||
local dirty = false
|
|
||||||
|
|
||||||
if bit32.btest(sub.flags, DIRTY) then
|
|
||||||
dirty = true
|
|
||||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
|
||||||
if update(dep) then
|
|
||||||
local subs = dep.subs :: Link
|
|
||||||
if subs.nextSub then
|
|
||||||
shallowPropagate(subs)
|
|
||||||
end
|
|
||||||
dirty = true
|
|
||||||
end
|
|
||||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
|
||||||
if link.nextSub or link.prevSub then
|
|
||||||
stack = { value = link, prev = stack }
|
|
||||||
end
|
|
||||||
link = dep.deps :: Link
|
|
||||||
sub = dep
|
|
||||||
checkDepth += 1
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
if not dirty and link.nextDep then
|
|
||||||
link = link.nextDep
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local continueTop = false
|
|
||||||
|
|
||||||
while checkDepth ~= 0 do
|
|
||||||
checkDepth -= 1
|
|
||||||
local firstSub = sub.subs :: Link
|
|
||||||
local hasMultipleSubs = firstSub.nextSub ~= nil
|
|
||||||
if hasMultipleSubs then
|
|
||||||
link = (stack :: Stack<Link>).value
|
|
||||||
stack = (stack :: Stack<Link>).prev
|
|
||||||
else
|
|
||||||
link = firstSub
|
|
||||||
end
|
|
||||||
if dirty then
|
|
||||||
if update(sub) then
|
|
||||||
if hasMultipleSubs then
|
|
||||||
shallowPropagate(firstSub)
|
|
||||||
end
|
|
||||||
sub = link.sub
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
else
|
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
|
|
||||||
end
|
|
||||||
sub = link.sub
|
|
||||||
if link.nextDep then
|
|
||||||
link = link.nextDep
|
|
||||||
continueTop = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
dirty = false
|
|
||||||
end
|
|
||||||
|
|
||||||
if continueTop then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
return dirty
|
|
||||||
until false
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
link = link,
|
|
||||||
unlink = unlink,
|
|
||||||
propagate = propagate,
|
|
||||||
checkDirty = checkDirty,
|
|
||||||
endTracking = endTracking,
|
|
||||||
startTracking = startTracking,
|
|
||||||
shallowPropagate = shallowPropagate,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
ReactiveFlags = ReactiveFlags,
|
|
||||||
createReactiveSystem = createReactiveSystem,
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
type Update<T> = ((previous: T) -> T) | T
|
|
||||||
export type Atom<T> = (...Update<T>) -> T
|
|
||||||
export type Selector<T> = () -> T
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local setCurrentTracking = core.setCurrentTracking
|
|
||||||
|
|
||||||
local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
|
||||||
setCurrentTracking(prevTracking)
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return untracked
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
local function handleNoYield(thread: thread, ok: boolean, ...)
|
|
||||||
if not ok then
|
|
||||||
return false, debug.traceback(thread, tostring((...)))
|
|
||||||
elseif coroutine.status(thread) ~= "dead" then
|
|
||||||
return false, debug.traceback(thread, "Attempted to yield!")
|
|
||||||
else
|
|
||||||
return true, ...
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
|
||||||
local thread = coroutine.create(callback)
|
|
||||||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
noYield = noYield :: typeof(pcall),
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "littensy/charm"
|
|
||||||
version = "0.10.0"
|
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
|
||||||
realm = "shared"
|
|
||||||
exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
|
|
@ -13,11 +13,11 @@ local function renderHook<Props, Result>(callback: (Props) -> Result, props: Pro
|
||||||
local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
|
local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
|
||||||
local hook = {}
|
local hook = {}
|
||||||
|
|
||||||
local function Component(props)
|
local function Component(props: Props)
|
||||||
hook.result = callback(props)
|
hook.result = callback(props)
|
||||||
end
|
end
|
||||||
|
|
||||||
function hook.rerender(newProps)
|
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))
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
suite.describe("react-charm", function()
|
suite.describe("useAtom", function()
|
||||||
if not game then
|
require("./useAtom.spec")
|
||||||
return suite.skip()
|
end)
|
||||||
end
|
|
||||||
suite.describe("useAtom", function()
|
suite.describe("useAtomBinding", function()
|
||||||
require("./useAtom.spec")
|
require("./useAtomBinding.spec")
|
||||||
end)
|
|
||||||
suite.describe("useAtomBinding", function()
|
|
||||||
require("./useAtomBinding.spec")
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
30
packages/react-charm/src/index.d.ts
vendored
30
packages/react-charm/src/index.d.ts
vendored
|
|
@ -1 +1,29 @@
|
||||||
export {};
|
import { Binding } from "@rbxts/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that subscribes to changes in the given atom or selector. The
|
||||||
|
* component is re-rendered with the new state after an update.
|
||||||
|
*
|
||||||
|
* If the `dependencies` array is provided, a new effect is created for the
|
||||||
|
* current `callback` when the dependencies change. Otherwise, the effect is
|
||||||
|
* created only once, when the component is mounted.
|
||||||
|
*
|
||||||
|
* @param callback The atom or selector to watch.
|
||||||
|
* @param dependencies An array of values used to memoize the callback.
|
||||||
|
* @returns The current state.
|
||||||
|
*/
|
||||||
|
export function useAtom<T>(callback: () => T, dependencies?: any[]): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that subscribes to changes in the given atom or selector. Returns
|
||||||
|
* a binding that updates when the state changes.
|
||||||
|
*
|
||||||
|
* If the `dependencies` array is provided, a new effect is created for the
|
||||||
|
* current `callback` when the dependencies change. Otherwise, the effect is
|
||||||
|
* created only once, when the component is mounted.
|
||||||
|
*
|
||||||
|
* @param callback The atom or selector to watch.
|
||||||
|
* @param dependencies An array of values used to memoize the callback.
|
||||||
|
* @returns A binding that stores the current state.
|
||||||
|
*/
|
||||||
|
export function useAtomBinding<T>(callback: () => T, dependencies?: any[]): Binding<T>;
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,21 @@ local useState = React.useState
|
||||||
local useMemo = React.useMemo
|
local useMemo = React.useMemo
|
||||||
|
|
||||||
--- A hook that subscribes to changes in the given atom or selector. The
|
--- A hook that subscribes to changes in the given atom or selector. The
|
||||||
--- component is re-rendered whenever the state changes.
|
--- component is re-rendered with the new state after an update.
|
||||||
---
|
---
|
||||||
--- If the `dependencies` array is provided, the effect is re-created whenever
|
--- If the `dependencies` array is provided, a new effect is created for the
|
||||||
--- the dependencies change. Otherwise, the subscription is created once when
|
--- current `callback` when the dependencies change. Otherwise, the effect is
|
||||||
--- the component is mounted.
|
--- created only once, when the component is mounted.
|
||||||
---
|
---
|
||||||
--- @param selector The atom or selector to subscribe to.
|
--- @param callback The atom or selector to watch.
|
||||||
--- @param dependencies An array of values that the subscription depends on.
|
--- @param dependencies An array of values used to memoize the callback.
|
||||||
--- @return The current state.
|
--- @return The current state.
|
||||||
local function useAtom<State>(selector: () -> State, dependencies: { any }?): State
|
local function useAtom<T>(callback: () -> T, dependencies: { any }?): T
|
||||||
local state, setState = useState(selector)
|
local state, setState = useState(callback)
|
||||||
|
|
||||||
useEffect(function()
|
useEffect(function()
|
||||||
return effect(function()
|
return effect(function()
|
||||||
setState(selector())
|
setState(callback())
|
||||||
end)
|
end)
|
||||||
end, dependencies or {})
|
end, dependencies or {})
|
||||||
|
|
||||||
|
|
@ -32,20 +32,20 @@ end
|
||||||
--- A hook that subscribes to changes in the given atom or selector. Returns
|
--- A hook that subscribes to changes in the given atom or selector. Returns
|
||||||
--- a binding that updates when the state changes.
|
--- a binding that updates when the state changes.
|
||||||
---
|
---
|
||||||
--- If the `dependencies` array is provided, the effect is re-created whenever
|
--- If the `dependencies` array is provided, a new effect is created for the
|
||||||
--- the dependencies change. Otherwise, the subscription is created once when
|
--- current `callback` when the dependencies change. Otherwise, the effect is
|
||||||
--- the component is mounted.
|
--- created only once, when the component is mounted.
|
||||||
---
|
---
|
||||||
--- @param selector The atom or selector to subscribe to.
|
--- @param callback The atom or selector to watch.
|
||||||
--- @param dependencies An array of values that the subscription depends on.
|
--- @param dependencies An array of values used to memoize the callback.
|
||||||
--- @return A binding that stores the current state.
|
--- @return A binding that stores the current state.
|
||||||
local function useAtomBinding<State>(selector: () -> State, dependencies: { any }?)
|
local function useAtomBinding<T>(callback: () -> T, dependencies: { any }?)
|
||||||
local initialState = useMemo(selector, {})
|
local initialState = useMemo(callback, {})
|
||||||
local binding, setBinding = useBinding(initialState)
|
local binding, setBinding = useBinding(initialState)
|
||||||
|
|
||||||
useEffect(function()
|
useEffect(function()
|
||||||
return effect(function()
|
return effect(function()
|
||||||
setBinding(selector())
|
setBinding(callback())
|
||||||
end)
|
end)
|
||||||
end, dependencies or {})
|
end, dependencies or {})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
suite.describe("vide-charm", function()
|
suite.describe("useAtom", function()
|
||||||
if not game then
|
require("./useAtom.spec")
|
||||||
return suite.skip()
|
|
||||||
end
|
|
||||||
suite.describe("useAtom", function()
|
|
||||||
require("./useAtom.spec")
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
9
packages/vide-charm/src/index.d.ts
vendored
9
packages/vide-charm/src/index.d.ts
vendored
|
|
@ -1 +1,8 @@
|
||||||
export {};
|
/**
|
||||||
|
* Subscribes to the state of an atom or selector. Returns a source that
|
||||||
|
* updates when the state changes.
|
||||||
|
*
|
||||||
|
* @param callback The atom or selector to subscribe to.
|
||||||
|
* @returns The reactive source.
|
||||||
|
*/
|
||||||
|
export function useAtom<T>(callback: () => T, dependencies?: any[]): () => T;
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ local cleanup = Vide.cleanup
|
||||||
--- Subscribes to the state of an atom or selector. Returns a source that
|
--- Subscribes to the state of an atom or selector. Returns a source that
|
||||||
--- updates when the state changes.
|
--- updates when the state changes.
|
||||||
---
|
---
|
||||||
--- @param selector The atom or selector to subscribe to.
|
--- @param callback The atom or selector to subscribe to.
|
||||||
--- @return The reactive source.
|
--- @return The reactive source.
|
||||||
local function useAtom<State>(selector: () -> State): () -> State
|
local function useAtom<State>(callback: () -> State): () -> State
|
||||||
local state = source(selector())
|
local state = source(callback())
|
||||||
cleanup(subscribe(selector, state))
|
cleanup(subscribe(callback, state))
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
local Charm = require("../packages/charm/src")
|
local Charm = require("../packages/charm/src")
|
||||||
local suite = require("./suite")
|
local suite = require("./suite")
|
||||||
|
|
||||||
Charm.strict.enabled = true
|
Charm.flags.strict = true
|
||||||
|
|
||||||
require("../packages/charm/src/__tests__/spec")
|
suite.describe("charm", function()
|
||||||
require("../packages/charm-sync/src/__tests__/spec")
|
require("../packages/charm/src/__tests__/spec")
|
||||||
require("../packages/react-charm/src/__tests__/spec")
|
end)
|
||||||
require("../packages/vide-charm/src/__tests__/spec")
|
|
||||||
|
suite.describe.ingame("charm-sync", function()
|
||||||
|
require("../packages/charm-sync/src/__tests__/spec")
|
||||||
|
end)
|
||||||
|
|
||||||
|
suite.describe.ingame("react-charm", function()
|
||||||
|
require("../packages/react-charm/src/__tests__/spec")
|
||||||
|
end)
|
||||||
|
|
||||||
|
suite.describe.ingame("vide-charm", function()
|
||||||
|
require("../packages/vide-charm/src/__tests__/spec")
|
||||||
|
end)
|
||||||
|
|
||||||
suite.finish()
|
suite.finish()
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2692eb055839cd56d0075ca72482658a5bdaddb9
|
Subproject commit eb95781a86e505cb6bff83ca568a67c7295ebb38
|
||||||
Loading…
Add table
Add a link
Reference in a new issue