From 77c98ca94ae21cdd15daa591c5f2c1bb17f0e098 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Sat, 5 Jul 2025 13:38:18 -0700 Subject: [PATCH] feat: add deep freeze flag, misc refactors --- benches/patch.bench.luau | 4 +- .../charm/src/__tests__/core/atom.spec.luau | 40 +++++++ .../src/__tests__/core/computed.spec.luau | 26 +++- .../charm/src/__tests__/core/gc.spec.luau | 28 ++--- .../src/__tests__/core/issue_48.spec.luau | 113 ------------------ .../src/__tests__/core/untrack.spec.luau | 23 ---- packages/charm/src/__tests__/listen.spec.luau | 17 +++ packages/charm/src/__tests__/peek.spec.luau | 107 ----------------- packages/charm/src/__tests__/spec.luau | 9 +- .../charm/src/__tests__/subscribe.spec.luau | 19 ++- .../charm/src/__tests__/untracked.spec.luau | 17 +-- packages/charm/src/core.luau | 11 ++ packages/charm/src/flags.luau | 10 +- packages/charm/src/index.d.ts | 13 +- packages/charm/src/init.luau | 10 +- packages/charm/src/listen.luau | 4 +- packages/charm/src/peek.luau | 18 --- packages/charm/src/subscribe.luau | 4 +- packages/charm/src/system.luau | 30 +---- packages/charm/src/untracked.luau | 8 +- packages/charm/src/utils.luau | 10 ++ test/spec.server.luau | 1 + 22 files changed, 185 insertions(+), 337 deletions(-) create mode 100644 packages/charm/src/__tests__/core/atom.spec.luau delete mode 100644 packages/charm/src/__tests__/core/issue_48.spec.luau delete mode 100644 packages/charm/src/__tests__/core/untrack.spec.luau delete mode 100644 packages/charm/src/__tests__/peek.spec.luau delete mode 100644 packages/charm/src/peek.luau diff --git a/benches/patch.bench.luau b/benches/patch.bench.luau index a4c5829..dc8f918 100644 --- a/benches/patch.bench.luau +++ b/benches/patch.bench.luau @@ -1,4 +1,6 @@ -local CharmSync = require("../packages/charm-sync/src") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local CharmSync = require(ReplicatedStorage.packages["charm-sync"].src) local patch = CharmSync.patch diff --git a/packages/charm/src/__tests__/core/atom.spec.luau b/packages/charm/src/__tests__/core/atom.spec.luau new file mode 100644 index 0000000..1c8f8d0 --- /dev/null +++ b/packages/charm/src/__tests__/core/atom.spec.luau @@ -0,0 +1,40 @@ +local core = require("../../core") +local suite = require("../../../../../test/suite") + +local atom = core.atom +local effect = core.effect +local test = suite.test + +test("should accept equality function", function() + local calls = {} + local src = atom(0, function(current, incoming) + return current == incoming or incoming % 2 == 1 + end) + + effect(function() + table.insert(calls, src()) + end) + + src(2) + src(3) + assert(src() == 2, "should skip odd value") + + src(4) + src(5) + assert(src() == 4, "should skip odd value") + assert(table.concat(calls) == "024", `should only call on even updates, got {table.concat(calls)}`) +end) + +test("should enforce immutability", function() + local src = atom({ nested = {} }) + assert(not pcall(function() + src().nested.a = 1 + end), "should not allow mutation of initial value") + + src({ nested = { a = 1 } }) + assert(not pcall(function() + src().nested.a = 2 + end), "should not allow mutation of new value") +end) + +return {} diff --git a/packages/charm/src/__tests__/core/computed.spec.luau b/packages/charm/src/__tests__/core/computed.spec.luau index c5f37d3..9c6e078 100644 --- a/packages/charm/src/__tests__/core/computed.spec.luau +++ b/packages/charm/src/__tests__/core/computed.spec.luau @@ -1,8 +1,9 @@ local core = require("../../core") local suite = require("../../../../../test/suite") -local computed = core.computed local atom = core.atom +local computed = core.computed +local effect = core.effect local test = suite.test test("should correctly propagate changes through computed core", function() @@ -80,4 +81,27 @@ test("should not update if the atom value is reverted", function() assert(times == 1, "computed ran when it should not") end) +test("should accept equality function", function() + local calls = {} + local src = atom(0) + local c1 = computed(function() + return src() + end, function(current, incoming) + return current == incoming or incoming % 2 == 1 + end) + + effect(function() + table.insert(calls, c1()) + end) + + src(2) + src(3) + assert(c1() == 2, "should skip odd value") + + src(4) + src(5) + assert(c1() == 4, "should skip odd value") + assert(table.concat(calls) == "024", `should only call on even updates, got {table.concat(calls)}`) +end) + return {} diff --git a/packages/charm/src/__tests__/core/gc.spec.luau b/packages/charm/src/__tests__/core/gc.spec.luau index 16bf679..d5b9bfc 100644 --- a/packages/charm/src/__tests__/core/gc.spec.luau +++ b/packages/charm/src/__tests__/core/gc.spec.luau @@ -8,15 +8,11 @@ local effectScope = core.effectScope local test = suite.test --- Creates a weak reference to a value. ---- @return Function to dereference the value. -local function weakRef(value: T): () -> T? - local ref = setmetatable({ value = value }, { __mode = "v" }) - return function() - return ref.value - end +local function weakRef(value: T): { value: T? } + return setmetatable({ value = value }, { __mode = "v" }) :: any end ---- Triggers garbage collection by allocating excessive memory. +--- Triggers garbage collection by creating a large number of tables. local function gc() for _ = 1, 1000 do local _ = table.create(100, 0) @@ -24,14 +20,14 @@ local function gc() end --- 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 ---- 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. +--- parent scope is disposed. However, calling a computed value outside of a +--- scope still links it to dependencies, which act as strong references. +--- Computed values called in this way won't GC until unlinked from a scope. --- --- This function releases a computed value by linking it to a scope and then --- immediately closing it, causing alien-signals to unlink the computed value ---- and allow it to be garbage collected. -local function release(getter: () -> ()) +--- from its dependencies and allow it to be garbage collected. +local function unlink(getter: () -> ()) effectScope(function() getter() end)() @@ -39,18 +35,17 @@ end test("should release computed cache", function() local srcRef = weakRef({}) - local src = atom(srcRef()) - + local src = atom(srcRef.value) local c = computed(function() return src() end) c() -- cache src value src(nil) -- release value - c = release(c) -- release computed + c = unlink(c) -- release computed gc() - assert(srcRef() == nil, "should release computed cache") + assert(srcRef.value == nil, "should release computed cache") end) test("should not release effect for atom", function() @@ -61,7 +56,6 @@ test("should not release effect for atom", function() effectCalls += 1 src() end) - assert(effectCalls == 1, "should call effect once") gc() diff --git a/packages/charm/src/__tests__/core/issue_48.spec.luau b/packages/charm/src/__tests__/core/issue_48.spec.luau deleted file mode 100644 index ba67e4e..0000000 --- a/packages/charm/src/__tests__/core/issue_48.spec.luau +++ /dev/null @@ -1,113 +0,0 @@ -local core = require("../../core") -local suite = require("../../../../../test/suite") - -local computed = core.computed -local effect = core.effect -local setCurrentSub = core.setCurrentSub -local atom = core.atom -local test = suite.test - -type ReactionOptions = { - fireImmediately: boolean?, - equals: ((a: T, b: T?) -> boolean)?, - onError: ((error: unknown) -> ())?, - scheduler: ((fn: () -> ()) -> ())?, - once: boolean?, -} - -local function untracked(callback: () -> T): T - local currentSub = setCurrentSub(nil) - local success, result = pcall(callback) - setCurrentSub(currentSub) - if not success then - error(result) - end - return result -end - -local function reaction( - dataFn: () -> T, - effectFn: (newValue: T, oldValue: T?) -> (), - options: ReactionOptions? -): () -> () - local options: ReactionOptions = options or {} - local scheduler: (() -> ()) -> () = options.scheduler or function(fn) - return fn() - end - local equals: (T, T?) -> boolean = options.equals or function(a, b) - return a == b - end - local onError = options.onError - local once = options.once or false - local fireImmediately = options.fireImmediately or false - - local prevValue: T? - local version = 0 - - local tracked = computed(function() - local success, result = pcall(dataFn) - if not success then - untracked(function() - if onError then - onError(result) - end - return nil - end) - return prevValue - end - return result - end) - - local dispose - dispose = effect(function() - local current = tracked() - if not fireImmediately and version == 0 then - prevValue = current - end - version += 1 - if equals(current :: T, prevValue) then - return - end - local oldValue = prevValue - prevValue = current - untracked(function() - scheduler(function() - local success = pcall(effectFn, current :: T, oldValue) - if not success and onError then - onError(error) - end - if once then - if (fireImmediately and version > 1) or (not fireImmediately and version > 0) then - dispose() - end - end - end) - return nil - end) - end) - - return dispose -end - -test("#48", function() - local source = atom(0) - local disposeInner: () -> () - - reaction(function() - return source() - end, function(val) - if val == 1 then - disposeInner = reaction(function() - return source() - end, function() end) - elseif val == 2 then - disposeInner() - end - end) - - source(1) - source(2) - source(3) -end) - -return {} diff --git a/packages/charm/src/__tests__/core/untrack.spec.luau b/packages/charm/src/__tests__/core/untrack.spec.luau deleted file mode 100644 index 177c3d7..0000000 --- a/packages/charm/src/__tests__/core/untrack.spec.luau +++ /dev/null @@ -1,23 +0,0 @@ -local core = require("../../core") -local suite = require("../../../../../test/suite") - -local computed = core.computed -local setCurrentSub = core.setCurrentSub -local atom = core.atom -local test = suite.test - -test("should pause tracking", function() - local src = atom(0) - local c = computed(function() - local currentSub = setCurrentSub(nil) - local value = src() - setCurrentSub(currentSub) - return value - end) - assert(c() == 0, "should not track src") - - src(1) - assert(c() == 0, "should not track src") -end) - -return {} diff --git a/packages/charm/src/__tests__/listen.spec.luau b/packages/charm/src/__tests__/listen.spec.luau index 097d2aa..d8c1764 100644 --- a/packages/charm/src/__tests__/listen.spec.luau +++ b/packages/charm/src/__tests__/listen.spec.luau @@ -98,4 +98,21 @@ test("should track inner subs", function() assert(table.concat(order) == "", `bad order: {table.concat(order)}`) end) +test("should skip redundant updates", function() + local order = {} + local a = atom(0) + + listen(function(): "even" | "odd" + return if a() % 2 == 0 then "even" else "odd" + end, function(value) + table.insert(order, value) + end) + + a(1) + a(3) + a(4) + a(6) + assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`) +end) + return {} diff --git a/packages/charm/src/__tests__/peek.spec.luau b/packages/charm/src/__tests__/peek.spec.luau deleted file mode 100644 index 0e12b96..0000000 --- a/packages/charm/src/__tests__/peek.spec.luau +++ /dev/null @@ -1,107 +0,0 @@ -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 {} diff --git a/packages/charm/src/__tests__/spec.luau b/packages/charm/src/__tests__/spec.luau index 4f63237..46bd916 100644 --- a/packages/charm/src/__tests__/spec.luau +++ b/packages/charm/src/__tests__/spec.luau @@ -1,6 +1,9 @@ local suite = require("../../../../test/suite") suite.describe("core", function() + suite.describe("atom", function() + require("./core/atom.spec") + end) suite.describe("computed", function() require("./core/computed.spec") end) @@ -10,18 +13,12 @@ suite.describe("core", function() suite.describe("effectScope", function() require("./core/effectScope.spec") end) - suite.describe("issue_48", function() - require("./core/issue_48.spec") - end) suite.describe("gc", function() require("./core/gc.spec") end) suite.describe("topology", function() require("./core/topology.spec") end) - suite.describe("untrack", function() - require("./core/untrack.spec") - end) end) suite.describe("listen", function() diff --git a/packages/charm/src/__tests__/subscribe.spec.luau b/packages/charm/src/__tests__/subscribe.spec.luau index d91a952..6d14a4d 100644 --- a/packages/charm/src/__tests__/subscribe.spec.luau +++ b/packages/charm/src/__tests__/subscribe.spec.luau @@ -103,8 +103,6 @@ test("should track inner subs", function() end) end) - assert(table.concat(order) == "", "should not run immediately") - a(a() + 1) assert(table.concat(order) == "123", `bad order: {table.concat(order)}`) @@ -118,4 +116,21 @@ test("should track inner subs", function() assert(table.concat(order) == "", `bad order: {table.concat(order)}`) end) +test("should skip redundant updates", function() + local order = {} + local a = atom(0) + + subscribe(function(): "even" | "odd" + return if a() % 2 == 0 then "even" else "odd" + end, function(value) + table.insert(order, value) + end) + + a(1) + a(3) + a(4) + a(6) + assert(table.concat(order) == "oddeven", `bad order: {table.concat(order)}`) +end) + return {} diff --git a/packages/charm/src/__tests__/untracked.spec.luau b/packages/charm/src/__tests__/untracked.spec.luau index 9d56084..7bc7f9f 100644 --- a/packages/charm/src/__tests__/untracked.spec.luau +++ b/packages/charm/src/__tests__/untracked.spec.luau @@ -5,27 +5,28 @@ local atom = Charm.atom local computed = Charm.computed local effect = Charm.effect local untracked = Charm.untracked -local onCleanup = Charm.onCleanup local test = suite.test -test("should run detached", function() +test("should not run detached", function() local a = atom(1) local spyCalls = 0 effect(function() a() untracked(function() - onCleanup(function() - spyCalls += 1 - end, true) + effect(function() + return function() + spyCalls += 1 + end + end) end) end) - assert(spyCalls == 0, "should not be called") + assert(spyCalls == 0, "should not be called yet") a(2) - assert(spyCalls == 0, "rerun should not call cleanup") + assert(spyCalls == 1, "should be called once") a(3) - assert(spyCalls == 0, "rerun should not call cleanup") + assert(spyCalls == 2, "should be called twice") end) test("should block tracking inside effects", function() diff --git a/packages/charm/src/core.luau b/packages/charm/src/core.luau index 49ff50b..4138aad 100644 --- a/packages/charm/src/core.luau +++ b/packages/charm/src/core.luau @@ -1,3 +1,7 @@ +-- Adapted from Alien Signals +-- https://github.com/stackblitz/alien-signals +-- https://github.com/Nicell/alien-signals-luau + local config = require("./flags") local system = require("./system") local utils = require("./utils") @@ -8,6 +12,7 @@ export type Link = system.Link local ReactiveFlags = system.ReactiveFlags local createReactiveSystem = system.createReactiveSystem +local deepFreeze = utils.deepFreeze local noYield = utils.noYield local NONE = 0b0000000 @@ -211,6 +216,9 @@ end local function writeAtom(atom: Atom, oldValue: T, newValue: T): T local equals = atom.equals if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then + if config.frozen then + deepFreeze(newValue) + end atom.value = newValue atom.flags = bit32.bor(MUTABLE, DIRTY) local subs = atom.subs @@ -323,6 +331,9 @@ local function unwatched(node: Atom | Computed | Effect | EffectScope) end local function atom(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T + if config.frozen then + deepFreeze(initialValue) + end return atomGetterSetter({ previousValue = initialValue, value = initialValue, diff --git a/packages/charm/src/flags.luau b/packages/charm/src/flags.luau index 961dd6f..b9d43d9 100644 --- a/packages/charm/src/flags.luau +++ b/packages/charm/src/flags.luau @@ -2,7 +2,13 @@ local function inlineTest(): string return debug.info(1, "n") end +local isO2 = inlineTest() ~= "inlineTest" + return { - -- Enable strict mode in O2 - strict = inlineTest() == "inlineTest", + --- Enforces synchronous, non-yielding behavior in atoms and effects. + --- Enabled if the optimize level is below 2, which is the case in studio. + strict = not isO2, + --- Enforces immutability of atoms by deep-freezing table values. Disabled + --- by default, but can be enabled to detect accidental mutations. + frozen = false, } diff --git a/packages/charm/src/index.d.ts b/packages/charm/src/index.d.ts index 3193856..6246761 100644 --- a/packages/charm/src/index.d.ts +++ b/packages/charm/src/index.d.ts @@ -12,6 +12,7 @@ export type Equals = (current: T, incoming: T) => boolean; export const flags: { strict: boolean; + frozen: boolean; }; export function atom(value: T, equals?: Equals): Atom; @@ -63,6 +64,14 @@ export function batched( ...args: Args ): (...args: Args) => Result; -export function peek(callback: (...args: Args) => Result, ...args: Args): Result; - export function untracked(callback: (...args: Args) => Result, ...args: Args): Result; + +/** + * @deprecated Use `batched()` instead. + */ +export const batch: typeof batched; + +/** + * @deprecated Use `untracked()` instead. + */ +export const peek: typeof untracked; diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index 7897bd9..b388e5d 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -4,7 +4,6 @@ local flags = require("@self/flags") local listen = require("@self/listen") local mapped = require("@self/mapped") local observe = require("@self/observe") -local peek = require("@self/peek") local subscribe = require("@self/subscribe") local types = require("@self/types") local untracked = require("@self/untracked") @@ -12,6 +11,11 @@ local untracked = require("@self/untracked") export type Atom = types.Atom export type Selector = types.Selector +--- @deprecated Function `batch()` is deprecated, use `batched()` instead. +local batch = batched +--- @deprecated Function `peek()` is deprecated, use `untracked()` instead. +local peek = untracked + return { core = core, flags = flags, @@ -24,7 +28,9 @@ return { listen = listen, mapped = mapped, observe = observe, - peek = peek, subscribe = subscribe, untracked = untracked, + -- Temporary aliases for backwards compatibility + batch = batch, + peek = peek, } diff --git a/packages/charm/src/listen.luau b/packages/charm/src/listen.luau index ed15d77..865e704 100644 --- a/packages/charm/src/listen.luau +++ b/packages/charm/src/listen.luau @@ -1,5 +1,5 @@ local core = require("./core") -local peek = require("./peek") +local untracked = require("./untracked") local computed = core.computed local effect = core.effect @@ -19,7 +19,7 @@ local function listen(selector: () -> T, listener: (state: T, previous: T?, d dispose = effect(function() local previous = current current = tracked() - peek(listener, current, previous, dispose) + untracked(listener, current, previous, dispose) end) if disposed then diff --git a/packages/charm/src/peek.luau b/packages/charm/src/peek.luau deleted file mode 100644 index a8bc08f..0000000 --- a/packages/charm/src/peek.luau +++ /dev/null @@ -1,18 +0,0 @@ -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(callback: (Args...) -> Results..., ...: Args...): Results... - return handlePeek(setCurrentTracking(false), pcall(callback, ...)) -end - -return peek diff --git a/packages/charm/src/subscribe.luau b/packages/charm/src/subscribe.luau index bd2b8c1..1ceca66 100644 --- a/packages/charm/src/subscribe.luau +++ b/packages/charm/src/subscribe.luau @@ -1,5 +1,5 @@ local core = require("./core") -local peek = require("./peek") +local untracked = require("./untracked") local computed = core.computed local effect = core.effect @@ -18,7 +18,7 @@ local function subscribe(selector: () -> T, listener: (state: T, lastState: T if mounted then mounted = false else - peek(listener, current, previous) + untracked(listener, current, previous) end end) end diff --git a/packages/charm/src/system.luau b/packages/charm/src/system.luau index d3d5692..dbd5e2c 100644 --- a/packages/charm/src/system.luau +++ b/packages/charm/src/system.luau @@ -1,30 +1,6 @@ ---[[ - 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. -]] +-- Adapted from Alien Signals +-- https://github.com/stackblitz/alien-signals +-- https://github.com/Nicell/alien-signals-luau export type ReactiveNode = { deps: Link?, diff --git a/packages/charm/src/untracked.luau b/packages/charm/src/untracked.luau index 80c2671..b766aae 100644 --- a/packages/charm/src/untracked.luau +++ b/packages/charm/src/untracked.luau @@ -1,9 +1,9 @@ local core = require("./core") -local setCurrentSub = core.setCurrentSub +local setCurrentTracking = core.setCurrentTracking -local function handleUntracked(prevSub: core.ReactiveNode?, ok: boolean, ...) - setCurrentSub(prevSub) +local function handleUntracked(prevTracking: boolean, ok: boolean, ...) + setCurrentTracking(prevTracking) if ok then return ... else @@ -12,7 +12,7 @@ local function handleUntracked(prevSub: core.ReactiveNode?, ok: boolean, ...) end local function untracked(callback: (Args...) -> Results..., ...: Args...): Results... - return handleUntracked(setCurrentSub(nil), pcall(callback, ...)) + return handleUntracked(setCurrentTracking(false), pcall(callback, ...)) end return untracked diff --git a/packages/charm/src/utils.luau b/packages/charm/src/utils.luau index 536e086..ce71ef5 100644 --- a/packages/charm/src/utils.luau +++ b/packages/charm/src/utils.luau @@ -13,6 +13,16 @@ local function noYield(callback: (Args...) -> Results..., . return handleNoYield(thread, coroutine.resume(thread, ...)) end +local function deepFreeze(object: any) + if type(object) == "table" and not table.isfrozen(object) then + table.freeze(object) + for _, value in object do + deepFreeze(value) + end + end +end + return { noYield = noYield :: typeof(pcall), + deepFreeze = deepFreeze, } diff --git a/test/spec.server.luau b/test/spec.server.luau index 46abc3b..ab0f072 100644 --- a/test/spec.server.luau +++ b/test/spec.server.luau @@ -2,6 +2,7 @@ local Charm = require("../packages/charm/src") local suite = require("./suite") Charm.flags.strict = true +Charm.flags.frozen = true suite.describe("charm", function() require("../packages/charm/src/__tests__/spec")