mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat!: use alien-signals for reactivity (#50)
* feat: use alien-signals * refactor: remove pnpm, fix tests * fix: `effectScope` should set scope * fix: check previous value before calling listener * refactor: improve imports * refactor: exclude re-exports from wally * refactor: embrace scope cleanup, restructure code * fix: update pkgs dir references * ci: simplify pesde install * build: install dev deps with wally * feat: add detached, add more tests * test: simplify mapped test update * chore: rephrase doc comment * perf: optimize subscribe and mapped * refactor: avoid ambiguous optional function type * refactor: add assertion error messages * perf: optimize mapped, add benches * chore: delete redundant benchmarks * perf: simplify mapped logic * test: prettify test library * test: try different gray * test: color brackets * refactor: sync 2.0.4 * refactor: use setCurrentSub instead of pauseTracking * test: improve test suite * test: separate parent from root * refactor: iterate over previous data in mapped * refactor: improve logic * style: use `---` doc comments * refactor: improve logic * chore: housekeeping * ci: add pnpm step * ci: init submodules * test: use lune * refactor: improve structure and alien-signals integration * ci: update test file path * feat: add peek and roblox-ts types * feat: add deep freeze flag, misc refactors * refactor: reduce type casting, housekeeping * refactor: rewrite untracked, update alien-signals * chore: update eslint, housekeeping * refactor: start charm-sync rewrite * fix: send init payload even if atom did not update * test: add all sync tests * feat: prepare for alpha release * feat: add peek, make untracked escape scope * fix: wrap hooks in untracked, update types * feat: add flag for inner effect tracking * refactor: add docs, remove some modules * refactor: align with alien-signals 3.0 * feat: add signal api * refactor: update charm sync api * feat: update alien-signals * refactor: remove unnecessary features, update docs * feat: finalize sync docs * docs: update sync section * docs: simplify examples * docs: capitalize headers * docs: add migration tips * docs: fix whitespace * docs: rephrase computed arg change * refactor: rename batch, misc fixes & perf updates * refactor: simplify charm sync validation * feat: update alien-signals, add recursion flag * feat: add `recursive()` * fix: allow dispose in observers * feat: add `trigger()`, refactor source * docs: update readme * chore: prepare release candidate * test: make some tests more reliable * docs: fix typo * feat: add `reactive()` * chore: bump versions * docs: reword reactive docs * docs: fix typo * feat: add `toRaw()` * fix: check for nil diffs * chore: bump versions * perf: do not create proxies for iteration * fix: make `toRaw` track all calls * fix: check none before patching mutable tables * chore: bump charm-sync version * docs: add fishing game example project * feat: sync signals that return reactive proxy * fix: refine reactive proxy logic * fix: prevent early reactive gc, alien-signals 3.1.1 * fix: wrap iterated values in `reactive()` * fix: loosen client signal types * chore: bump release candidate * fix: remove unsynced signals immediately * test: use lute for tests * chore: bump versions * ci: set up lute * refactor: improve source readability * ci: upgrade lute * build: loosen workspace version requirements * chore: bump deps * fix: update signal overload for setter (#58) * fix: add missing recursed check * test: add important vue reactivity tests * chore: fix react tests, run stylua * chore: update vide * docs: improve intro and explain getters * docs: adjust ordering by importance * docs: move listen nesting note * build: fix react peer dep for ci * chore: update lockfile * ci: use javascript eslint config * ci: ignore eslint config file * refactor: move test directories * feat: allow recursion in effects by default * release: charm v0.11.0-rc.4 * refactor: remove recursive() * refactor: reorganize repo, create deep-charm * docs: rephrase some sections * feat: export getter and setter types * docs: rephrase intro again * feat!: run listen and subscribe callback in untracked * refactor!: remove peek * docs: note peek in migration guide * release: charm v0.11.0-rc.5 * release: charm-sync v0.4.0-rc.7 * release: react-charm v0.4.0-rc.4 * fix: remove old keys when updating key in `mapped()` * refactor: shorten error traceback, housekeeping * refactor: keep error level in some cases * fix: check if cleanup functions disposed effect * refactor!: run effects in batched * ci: check eslint and prettier * perf: run observe fn in untracked * perf: call mapper as-is in `mapped()` * refactor: error for effect cleanup errors * refactor: use new type solver * ci: disable new solver flag until 0.714 * chore: housekeeping * refactor: improve errors in nonstrict, optimize noyield * chore: housekeeping * refactor: satisfy new solver * refactor: improve error traceback * docs: link roblox signals * chore: housekeeping * perf: remove `clearPending` inline operation * refactor: check type of cleanup * ci: remove selene * fix: always return pending value in signal setter * test: add multi benchmark * release: charm v0.11.0-rc.6 * test: add cleanup order tests * chore: bump deps * docs: add warning for deep-charm and note for `observe()` --------- Co-authored-by: safazi <safazics@gmail.com>
This commit is contained in:
parent
d0b7cff943
commit
573533058f
148 changed files with 9222 additions and 4384 deletions
|
|
@ -1,30 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local computed = Charm.computed
|
||||
local atom = Charm.atom
|
||||
|
||||
return {
|
||||
ParameterGenerator = function() end,
|
||||
Functions = {
|
||||
atom = function()
|
||||
local state = atom(0)
|
||||
|
||||
for _ = 1, 500 do
|
||||
state(math.random(1, 2))
|
||||
end
|
||||
end,
|
||||
|
||||
computed = function()
|
||||
local state = atom(0)
|
||||
|
||||
local _double = computed(function()
|
||||
return 2 * state()
|
||||
end)
|
||||
|
||||
for _ = 1, 500 do
|
||||
state(math.random(1, 2))
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
local USERS = 1000
|
||||
|
||||
local function generate()
|
||||
local users = {}
|
||||
|
||||
for user = 1, USERS do
|
||||
users[user] = {
|
||||
coins = math.random(1, 2),
|
||||
gems = math.random(1, 2),
|
||||
}
|
||||
end
|
||||
|
||||
return { users = users }
|
||||
end
|
||||
|
||||
local function update(state)
|
||||
state = table.clone(state)
|
||||
state.users = table.clone(state.users)
|
||||
|
||||
for user = 1, USERS do
|
||||
if math.random() > 0.5 then
|
||||
continue
|
||||
end
|
||||
|
||||
state.users[user] = {
|
||||
coins = math.random(1, 2),
|
||||
gems = math.random(1, 2),
|
||||
}
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local state = generate()
|
||||
return state, update(state)
|
||||
end,
|
||||
Functions = {
|
||||
diff = function(_, a, b)
|
||||
patch.diff(a, b)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
local client = CharmSync.client
|
||||
|
||||
return function()
|
||||
it("syncs state payload", function()
|
||||
local a = atom(1)
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = 2 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("syncs state patch", function()
|
||||
local a = atom({ b = 1, c = 2 })
|
||||
local previous = a()
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a() },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(previous)
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = { b = 3 } },
|
||||
})
|
||||
|
||||
expect(a()).to.never.equal(previous)
|
||||
expect(a().b).to.equal(3)
|
||||
expect(a().c).to.equal(2)
|
||||
end)
|
||||
|
||||
it("fixes sparse array patches", function()
|
||||
local a = atom({ 1 })
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a() },
|
||||
})
|
||||
|
||||
-- This patch would be the result of a diff between { 1 } and { 1, 2 } that
|
||||
-- was JSON-serialized and deserialized. The client should be able to catch
|
||||
-- this and convert the string key back to a number.
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = { ["2"] = 2 } },
|
||||
})
|
||||
|
||||
expect(a()["2" :: any]).to.never.be.ok()
|
||||
expect(a()[2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("applies multiple patches", function()
|
||||
local a = atom(1)
|
||||
local client = client({ atoms = { a = a } })
|
||||
local calls = {}
|
||||
|
||||
subscribe(a, function(value)
|
||||
table.insert(calls, value)
|
||||
end)
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a() },
|
||||
})
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = 2 },
|
||||
}, {
|
||||
type = "patch",
|
||||
data = { a = 3 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(3)
|
||||
expect(calls[1]).to.equal(2)
|
||||
expect(calls[2]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("batches patches", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local client = client({ atoms = { a = a, b = b } })
|
||||
local states = {}
|
||||
|
||||
subscribe(a, function()
|
||||
states.b = b()
|
||||
end)
|
||||
|
||||
subscribe(b, function()
|
||||
states.a = a()
|
||||
end)
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a(), b = b() },
|
||||
})
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = 1, b = 2 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(1)
|
||||
expect(b()).to.equal(2)
|
||||
expect(states.a).to.equal(1)
|
||||
expect(states.b).to.equal(2)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
local flatten = CharmSync.flatten
|
||||
|
||||
return function()
|
||||
it("flattens atoms", function()
|
||||
local input = {
|
||||
a = atom(0),
|
||||
b = { c = atom(0) },
|
||||
d = { e = { f = atom(0) } },
|
||||
}
|
||||
local result = flatten(input)
|
||||
|
||||
expect(result.a).to.equal(input.a)
|
||||
expect(result["b/c"]).to.equal(input.b.c)
|
||||
expect(result["d/e/f"]).to.equal(input.d.e.f)
|
||||
|
||||
expect(result.b).to.never.be.ok()
|
||||
expect(result.d).to.never.be.ok()
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
local HttpService = game:GetService("HttpService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local function serdes(value: { [any]: any })
|
||||
return HttpService:JSONDecode(HttpService:JSONEncode(value))
|
||||
end
|
||||
|
||||
it("returns a patch", function()
|
||||
local prevState = { foo = "bar", baz = "qux" }
|
||||
local nextState = { foo = "bar", baz = "quux" }
|
||||
local patches = patch.diff(prevState, nextState)
|
||||
|
||||
expect(patches).to.be.ok()
|
||||
expect(patches.baz).to.equal("quux")
|
||||
expect(patches.foo).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("applies a patch", function()
|
||||
local prevState = { foo = "bar", baz = "qux" }
|
||||
local patches = { baz = "quux", qaz = "qux" }
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
expect(applied).to.be.ok()
|
||||
expect(applied.baz).to.equal("quux")
|
||||
expect(applied.foo).to.equal("bar")
|
||||
expect(applied.qaz).to.equal("qux")
|
||||
end)
|
||||
|
||||
it("allows nested patches", function()
|
||||
local prevState = { foo = { bar = "baz" } }
|
||||
local nextState = { foo = { bar = "qux" } }
|
||||
local patches = patch.diff(prevState, nextState)
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
expect(applied.foo.bar).to.equal("qux")
|
||||
end)
|
||||
|
||||
it("preserves array density when applying", function()
|
||||
local prevState = { "a", "b", "c", "d" }
|
||||
local nextState = { ["2"] = "B", ["4"] = "D" }
|
||||
local applied = patch.apply(prevState, nextState)
|
||||
|
||||
expect(applied[1]).to.equal("a")
|
||||
expect(applied[2]).to.equal("B")
|
||||
expect(applied[3]).to.equal("c")
|
||||
expect(applied[4]).to.equal("D")
|
||||
|
||||
expect(applied["2"]).to.never.be.ok()
|
||||
expect(applied["4"]).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("preserves array density after serdes", function()
|
||||
-- This particular change results in a patch that cannot be serialized
|
||||
-- without losing information. Charm resolves this issue internally.
|
||||
local prevState = { "a", "b", "c", "d", "e" }
|
||||
local nextState = { "A", "b", "c", "D" }
|
||||
|
||||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
for index in next, prevState do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
|
||||
for index in next, applied do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
end)
|
||||
|
||||
it("preserves large gaps between keys", function()
|
||||
local prevState = table.create(10000, "foo")
|
||||
local nextState = table.move(prevState, 2, 9998, 2, { [1] = "bar", [9999] = "bar" })
|
||||
|
||||
local start = os.clock()
|
||||
|
||||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
assert(os.clock() - start < 10e-3, `Performance is not acceptable ({(os.clock() - start) * 1e3}ms)`)
|
||||
|
||||
for index in next, prevState do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
end)
|
||||
|
||||
it("cannot preserve sparse arrays", function()
|
||||
local prevState = {}
|
||||
local nextState = { [2] = "foo" }
|
||||
|
||||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
-- Not enough information is available to determine whether 'patches'
|
||||
-- should keep the string keys added during remote event serialization.
|
||||
expect(applied["2"]).to.be.ok()
|
||||
expect(applied[2]).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("optionally fixes sparse arrays", function()
|
||||
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }, true) do
|
||||
expect(index).to.be.a("string")
|
||||
end
|
||||
|
||||
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }, false) do
|
||||
expect(index).to.be.a("number")
|
||||
end
|
||||
end)
|
||||
|
||||
it("optionally validates state", function()
|
||||
expect(function()
|
||||
patch.diff({}, { [0.1] = true }, true)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
patch.diff({}, { [0.1] = true }, false)
|
||||
end).to.never.throw()
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
type SyncPayload = CharmSync.SyncPayload
|
||||
local server = CharmSync.server
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local target: Player = {} :: any
|
||||
|
||||
it("sends state to new players", function()
|
||||
local a = atom(1)
|
||||
local server = server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: SyncPayload
|
||||
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
server:hydrate(target)
|
||||
|
||||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("init")
|
||||
expect(payload.data.a).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("sends state patches", function()
|
||||
local a = atom({ b = 1, c = 2 } :: { [string]: number? })
|
||||
local server = server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: SyncPayload
|
||||
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
a({ b = 1, c = 5 })
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("patch")
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(payload.data.a.c).to.equal(5)
|
||||
|
||||
a({ b = 1, d = 1 })
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(patch.isNone(payload.data.a.c)).to.equal(true)
|
||||
expect(payload.data.a.d).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("preserves history", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local payloads: { SyncPayload }
|
||||
|
||||
local server = server({
|
||||
atoms = { a = a, b = b },
|
||||
interval = -1,
|
||||
preserveHistory = true,
|
||||
})
|
||||
|
||||
local disconnect = server:connect(function(_, ...)
|
||||
payloads = { ... }
|
||||
end)
|
||||
|
||||
a(2)
|
||||
b(2)
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(#payloads).to.equal(1)
|
||||
expect(payloads[1].type).to.equal("patch")
|
||||
expect(payloads[1].data.a).to.equal(2)
|
||||
expect(payloads[1].data.b).to.equal(2)
|
||||
|
||||
a(3)
|
||||
a(4)
|
||||
b(3)
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(#payloads).to.equal(2)
|
||||
expect(payloads[1].type).to.equal("patch")
|
||||
expect(payloads[1].data.a).to.equal(3)
|
||||
expect(payloads[1].data.b).to.never.be.ok()
|
||||
expect(payloads[2].type).to.equal("patch")
|
||||
expect(payloads[2].data.a).to.equal(4)
|
||||
expect(payloads[2].data.b).to.equal(3)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local validate = require(ReplicatedStorage.DevPackages.CharmSync.validate)
|
||||
|
||||
return function()
|
||||
it("validates inputs", function()
|
||||
local cases: { [string]: { value: any, expected: any } } = {
|
||||
array = { value = { 1, 2, 3 }, expected = true },
|
||||
array_sparse = { value = { 1, nil :: any, 3 }, expected = true },
|
||||
array_sparse_invalid = { value = { [1] = 0, [3] = 0 }, expected = false },
|
||||
|
||||
map_decimal_key = { value = { [1.5] = 0 }, expected = false },
|
||||
map_infinity_key = { value = { [math.huge] = true }, expected = false },
|
||||
map_mixed_key = { value = { [1] = 0, a = 0 }, expected = false },
|
||||
|
||||
nested_atom = { value = atom(0), expected = false },
|
||||
}
|
||||
|
||||
for key, case in next, cases do
|
||||
local success, result = pcall(function()
|
||||
validate(case.value, key)
|
||||
end)
|
||||
|
||||
if case.expected and not success then
|
||||
error(`Expected {key} to be valid, threw {result}`)
|
||||
elseif not case.expected and success then
|
||||
error(`Expected {key} to be invalid, but it was valid`)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("updates state", function()
|
||||
local source = atom(1)
|
||||
source(2)
|
||||
expect(source()).to.equal(2)
|
||||
source(function(x)
|
||||
return x + 1
|
||||
end)
|
||||
expect(source()).to.equal(3)
|
||||
end)
|
||||
|
||||
it("calls listeners", function()
|
||||
local source = atom(1)
|
||||
local value
|
||||
local function listener()
|
||||
value = source()
|
||||
end
|
||||
store.listeners[source][listener] = true
|
||||
source(2)
|
||||
expect(value).to.equal(2)
|
||||
end)
|
||||
|
||||
it("skips pending listeners", function()
|
||||
local source = atom(1)
|
||||
local outer = 0
|
||||
local inner = 0
|
||||
local function listener()
|
||||
outer += 1
|
||||
local function listener()
|
||||
inner += 1
|
||||
end
|
||||
store.listeners[source][listener] = true
|
||||
end
|
||||
store.listeners[source][listener] = true
|
||||
source(2)
|
||||
expect(outer).to.equal(1)
|
||||
expect(inner).to.equal(0)
|
||||
source(3)
|
||||
expect(outer).to.equal(2)
|
||||
expect(inner).to.equal(1)
|
||||
end)
|
||||
|
||||
it("takes a custom comparator", function()
|
||||
local max = atom(1, {
|
||||
equals = function(prev, next)
|
||||
return next < prev
|
||||
end,
|
||||
})
|
||||
max(2)
|
||||
expect(max()).to.equal(2)
|
||||
max(1)
|
||||
expect(max()).to.equal(2)
|
||||
max(3)
|
||||
expect(max()).to.equal(3)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local effect = Charm.effect
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("batches updates", function()
|
||||
local source = atom(1)
|
||||
local value = 0
|
||||
subscribe(source, function()
|
||||
value += 1
|
||||
end)
|
||||
store.batch(function()
|
||||
source(2)
|
||||
source(3)
|
||||
source(4)
|
||||
end)
|
||||
expect(value).to.equal(1)
|
||||
end)
|
||||
|
||||
it("handles duplicate listeners", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local value = 0
|
||||
effect(function()
|
||||
value += 1
|
||||
a()
|
||||
b()
|
||||
end)
|
||||
value = 0 -- effect() will run once initially
|
||||
store.batch(function()
|
||||
a(2)
|
||||
b(2)
|
||||
a(3)
|
||||
end)
|
||||
expect(value).to.equal(1)
|
||||
end)
|
||||
|
||||
it("batches computed atoms", function()
|
||||
local source = atom(1)
|
||||
local updates = 0
|
||||
local computations = 0
|
||||
local double = computed(function()
|
||||
computations += 1
|
||||
return source() * 2
|
||||
end)
|
||||
computations = 0 -- computed atoms will run once initially
|
||||
subscribe(double, function()
|
||||
updates += 1
|
||||
end)
|
||||
store.batch(function()
|
||||
source(2)
|
||||
source(3)
|
||||
source(4)
|
||||
end)
|
||||
expect(updates).to.equal(1)
|
||||
expect(computations).to.equal(1)
|
||||
expect(double()).to.equal(8)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("gracefully handles yielding", function()
|
||||
expect(function()
|
||||
store.capture(function()
|
||||
task.wait(10)
|
||||
return 1
|
||||
end)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("captures atoms from a function", function()
|
||||
local a, b, c = atom(1), atom(1), atom(2)
|
||||
local deps, result = store.capture(function()
|
||||
return a() + b()
|
||||
end)
|
||||
expect(deps[a]).to.be.ok()
|
||||
expect(deps[b]).to.be.ok()
|
||||
expect(deps[c]).to.never.be.ok()
|
||||
expect(result).to.equal(2)
|
||||
end)
|
||||
|
||||
it("captures an atom by itself", function()
|
||||
local a = atom(1)
|
||||
local deps, result = store.capture(a)
|
||||
expect(deps[a]).to.be.ok()
|
||||
expect(result).to.equal(1)
|
||||
end)
|
||||
|
||||
it("skips atoms in inner capture calls", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local inner
|
||||
local outer = store.capture(function()
|
||||
inner = store.capture(function()
|
||||
return b()
|
||||
end)
|
||||
return a()
|
||||
end)
|
||||
|
||||
assert(outer[a], "outer did not capture a")
|
||||
assert(outer[b] == nil, "outer captured b")
|
||||
assert(inner[a] == nil, "inner captured a")
|
||||
assert(inner[b], "inner did not capture b")
|
||||
end)
|
||||
|
||||
it("should not capture notified dependents", function()
|
||||
local state = atom(1)
|
||||
|
||||
subscribe(state, function() end)
|
||||
|
||||
local deps = store.capture(function()
|
||||
return state(2)
|
||||
end)
|
||||
|
||||
expect(deps[state]).to.never.be.ok()
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
local collect = require(script.Parent.Parent.utils.collect)
|
||||
local count = require(script.Parent.Parent.utils.count)
|
||||
|
||||
return function()
|
||||
it("memoizes the result", function()
|
||||
local calls = 0
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local double = computed(function()
|
||||
calls += 1
|
||||
return a() * b()
|
||||
end)
|
||||
expect(calls).to.equal(1)
|
||||
a(1)
|
||||
expect(calls).to.equal(1)
|
||||
a(2)
|
||||
expect(calls).to.equal(2)
|
||||
b(3)
|
||||
expect(calls).to.equal(3)
|
||||
double()
|
||||
expect(calls).to.equal(3)
|
||||
end)
|
||||
|
||||
it("can be nested", function()
|
||||
local source = atom(1)
|
||||
local double = computed(function()
|
||||
return source() * 2
|
||||
end)
|
||||
local quadruple = computed(function()
|
||||
return double() * 2
|
||||
end)
|
||||
expect(quadruple()).to.equal(4)
|
||||
source(2)
|
||||
expect(quadruple()).to.equal(8)
|
||||
end)
|
||||
|
||||
it("collects computed atoms", function()
|
||||
local source = atom(0)
|
||||
|
||||
computed(function()
|
||||
return source()
|
||||
end)
|
||||
|
||||
collect()
|
||||
|
||||
assert(count(store.listeners[source]) == 0, "computed atom did not gc")
|
||||
end)
|
||||
|
||||
it("collects nested computed atoms", function()
|
||||
local source = atom(0)
|
||||
|
||||
do
|
||||
-- Declare this in another scope to ensure no references exist at the
|
||||
-- time of garbage collection.
|
||||
local double = computed(function()
|
||||
return source() * 2
|
||||
end)
|
||||
|
||||
computed(function()
|
||||
return double() * 2
|
||||
end)
|
||||
end
|
||||
|
||||
collect()
|
||||
|
||||
assert(count(store.listeners[source]) == 0, "nested computed atom did not gc")
|
||||
end)
|
||||
|
||||
it("tracks conditional dependencies", function()
|
||||
local condition = atom(false)
|
||||
local a = atom(0)
|
||||
local b = atom(1)
|
||||
local reruns = 0
|
||||
local current
|
||||
|
||||
-- assign variable to avoid gc
|
||||
local _ = computed(function()
|
||||
reruns += 1
|
||||
current = if condition() then a() else b()
|
||||
return current
|
||||
end)
|
||||
|
||||
expect(reruns).to.equal(1)
|
||||
expect(current).to.equal(1)
|
||||
|
||||
condition(true)
|
||||
expect(reruns).to.equal(2)
|
||||
expect(current).to.equal(0)
|
||||
|
||||
a(1)
|
||||
b(2)
|
||||
expect(reruns).to.equal(3)
|
||||
expect(current).to.equal(1)
|
||||
|
||||
condition(false)
|
||||
expect(reruns).to.equal(4)
|
||||
expect(current).to.equal(2)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local effect = Charm.effect
|
||||
|
||||
return function()
|
||||
it("runs on state change", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local current
|
||||
local unsubscribe = effect(function()
|
||||
current = a() + b()
|
||||
end)
|
||||
|
||||
expect(current).to.equal(0)
|
||||
|
||||
for index = 1, 3 do
|
||||
a(index)
|
||||
expect(current).to.equal(index)
|
||||
end
|
||||
|
||||
for index = 1, 3 do
|
||||
b(index)
|
||||
expect(current).to.equal(a() + index)
|
||||
end
|
||||
|
||||
unsubscribe()
|
||||
a(4)
|
||||
expect(current).to.equal(6)
|
||||
end)
|
||||
|
||||
it("accepts a cleanup function", function()
|
||||
local a = atom(0)
|
||||
local cleanups = 0
|
||||
local unsubscribe = effect(function()
|
||||
a()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
|
||||
expect(cleanups).to.equal(0)
|
||||
a(1)
|
||||
expect(cleanups).to.equal(1)
|
||||
unsubscribe()
|
||||
expect(cleanups).to.equal(2)
|
||||
end)
|
||||
|
||||
it("tracks conditional dependencies", function()
|
||||
local condition = atom(false)
|
||||
local a = atom(0)
|
||||
local b = atom(1)
|
||||
local reruns = 0
|
||||
local current
|
||||
|
||||
effect(function()
|
||||
reruns += 1
|
||||
current = if condition() then a() else b()
|
||||
end)
|
||||
|
||||
expect(reruns).to.equal(1)
|
||||
expect(current).to.equal(1)
|
||||
|
||||
condition(true)
|
||||
expect(reruns).to.equal(2)
|
||||
expect(current).to.equal(0)
|
||||
|
||||
a(1)
|
||||
b(2)
|
||||
expect(reruns).to.equal(3)
|
||||
expect(current).to.equal(1)
|
||||
|
||||
condition(false)
|
||||
expect(reruns).to.equal(4)
|
||||
expect(current).to.equal(2)
|
||||
end)
|
||||
|
||||
it("allows self-cleanup", function()
|
||||
local a = atom(1)
|
||||
local cleanedUp = false
|
||||
local cleanup
|
||||
|
||||
cleanup = effect(function()
|
||||
if cleanup then
|
||||
assert(not cleanedUp, "cleanup ran twice")
|
||||
cleanup()
|
||||
cleanedUp = true
|
||||
end
|
||||
a()
|
||||
end)
|
||||
|
||||
a(2)
|
||||
a(3)
|
||||
end)
|
||||
|
||||
it("allows immediate self-cleanup", function()
|
||||
local a = atom(1)
|
||||
local cleanedUp = false
|
||||
|
||||
effect(function(cleanup)
|
||||
assert(not cleanedUp, "cleanup ran twice")
|
||||
cleanup()
|
||||
cleanedUp = true
|
||||
a()
|
||||
end)
|
||||
|
||||
a(2)
|
||||
a(3)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local mapped = Charm.mapped
|
||||
local observe = Charm.observe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
it("returns mapped atoms", function()
|
||||
local itemsAtom = atom({ "a", "b", "c" })
|
||||
local mappedAtom = mapped(itemsAtom, function(value, key)
|
||||
return string.upper(value), key
|
||||
end)
|
||||
local updates = 0
|
||||
|
||||
store.listeners[mappedAtom][function()
|
||||
updates += 1
|
||||
end] = true
|
||||
|
||||
for key = 1, 3 do
|
||||
local item = mappedAtom()[key]
|
||||
expect(item).to.equal(string.upper(itemsAtom()[key]))
|
||||
end
|
||||
|
||||
itemsAtom({ "d", "e", "f" })
|
||||
expect(updates).to.equal(1)
|
||||
|
||||
for key = 1, 3 do
|
||||
local item = mappedAtom()[key]
|
||||
expect(item).to.equal(string.upper(itemsAtom()[key]))
|
||||
end
|
||||
|
||||
itemsAtom({ [1] = "d", [3] = "f" })
|
||||
expect(mappedAtom()[2]).to.never.be.ok()
|
||||
expect(updates).to.equal(2)
|
||||
end)
|
||||
|
||||
it("uses key extractor", function()
|
||||
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
|
||||
local mappedAtom = mapped(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
for key = 1, 3 do
|
||||
local item = mappedAtom()[key]
|
||||
expect(item).to.equal(itemsAtom()[key])
|
||||
end
|
||||
|
||||
local previous = mappedAtom()
|
||||
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
|
||||
|
||||
expect(mappedAtom()[1]).to.equal(previous[1])
|
||||
expect(mappedAtom()[1]).to.equal(itemsAtom()[1])
|
||||
expect(mappedAtom()[2]).to.never.be.ok()
|
||||
expect(mappedAtom()[3]).to.equal(previous[3])
|
||||
expect(mappedAtom()[3]).to.equal(itemsAtom()[2])
|
||||
end)
|
||||
|
||||
it("allows observe()", function()
|
||||
local itemsAtom = atom({
|
||||
{ key = 1, value = "a" },
|
||||
{ key = 2, value = "b" },
|
||||
{ key = 3, value = "c" },
|
||||
})
|
||||
|
||||
local mappedAtom = mapped(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
local added = 0
|
||||
local deleted = 0
|
||||
|
||||
observe(mappedAtom, function(item, key)
|
||||
expect(item).to.equal(itemsAtom()[key])
|
||||
added += 1
|
||||
|
||||
return function()
|
||||
expect(mappedAtom()[key]).to.never.be.ok()
|
||||
deleted += 1
|
||||
end
|
||||
end)
|
||||
|
||||
expect(added).to.equal(3)
|
||||
expect(deleted).to.equal(0)
|
||||
|
||||
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
|
||||
|
||||
expect(added).to.equal(3)
|
||||
expect(deleted).to.equal(1)
|
||||
|
||||
itemsAtom({})
|
||||
|
||||
expect(added).to.equal(3)
|
||||
expect(deleted).to.equal(3)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local observe = Charm.observe
|
||||
|
||||
return function()
|
||||
it("observes lifetime", function()
|
||||
local itemsAtom = atom({ "a", "b", "c" })
|
||||
local state = {}
|
||||
|
||||
local cleanup = observe(itemsAtom, function(item, key)
|
||||
state[key] = item
|
||||
return function()
|
||||
state[key] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
for key = 1, 3 do
|
||||
expect(state[key]).to.equal(itemsAtom()[key])
|
||||
end
|
||||
|
||||
itemsAtom({ [4] = "d", [5] = "e", [6] = "f" })
|
||||
|
||||
for key = 1, 6 do
|
||||
expect(state[key]).to.equal(itemsAtom()[key])
|
||||
end
|
||||
|
||||
cleanup()
|
||||
|
||||
for key = 1, 6 do
|
||||
expect(state[key]).to.equal(nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local capture = Charm.capture
|
||||
local peek = Charm.peek
|
||||
|
||||
return function()
|
||||
it("calls an atom without capturing it", function()
|
||||
local a, b, c = atom(1), atom(1), atom(1)
|
||||
local deps, result = capture(function()
|
||||
return a() + peek(b) + c()
|
||||
end)
|
||||
expect(deps[a]).to.be.ok()
|
||||
expect(deps[b]).to.never.be.ok()
|
||||
expect(deps[c]).to.be.ok()
|
||||
expect(result).to.equal(3)
|
||||
end)
|
||||
|
||||
it("passes through arguments", function()
|
||||
expect(peek(1)).to.equal(1)
|
||||
expect(peek(function()
|
||||
return 1
|
||||
end)).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
local collect = require(script.Parent.Parent.utils.collect)
|
||||
local count = require(script.Parent.Parent.utils.count)
|
||||
|
||||
return function()
|
||||
it("runs on state change", function()
|
||||
local current, previous
|
||||
local source = atom(0)
|
||||
local unsubscribe = subscribe(source, function(...)
|
||||
current, previous = ...
|
||||
end)
|
||||
|
||||
for index = 1, 3 do
|
||||
source(index)
|
||||
expect(current).to.equal(index)
|
||||
expect(previous).to.equal(index - 1)
|
||||
end
|
||||
|
||||
unsubscribe()
|
||||
source(4)
|
||||
|
||||
expect(current).to.equal(3)
|
||||
expect(previous).to.equal(2)
|
||||
end)
|
||||
|
||||
it("prevents garbage collect", function()
|
||||
local source = atom(1)
|
||||
local updates = 0
|
||||
|
||||
-- Create a new scope to encapsulate variables with a limited lifetime, like
|
||||
-- 'unsubscribe' and 'computed', to ensure they are garbage collected.
|
||||
--
|
||||
-- The idea here is to test whether subscribe() prevents every computed
|
||||
-- atom in a chain from being garbage collected. If any are GCed, then
|
||||
-- the subscription will stop updating and the test will fail.
|
||||
do
|
||||
local unsubscribe
|
||||
|
||||
do
|
||||
local double = computed(function()
|
||||
return source() * 2
|
||||
end)
|
||||
|
||||
unsubscribe = subscribe(
|
||||
computed(function()
|
||||
return double() * 2
|
||||
end),
|
||||
function()
|
||||
updates += 1
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
collect()
|
||||
source(source() + 1)
|
||||
|
||||
assert(updates == 1, "subscription stopped updating")
|
||||
assert(count(store.listeners[source]) == 1, "computed atom was garbage collected")
|
||||
|
||||
unsubscribe()
|
||||
end
|
||||
|
||||
collect()
|
||||
|
||||
assert(count(store.listeners[source]) == 0, "computed atom did not collect")
|
||||
end)
|
||||
|
||||
it("tracks conditional dependencies", function()
|
||||
local condition = atom(false)
|
||||
local a = atom(0)
|
||||
local b = atom(1)
|
||||
local reruns = 0
|
||||
local current
|
||||
|
||||
subscribe(function()
|
||||
return if condition() then a() else b()
|
||||
end, function(state)
|
||||
reruns += 1
|
||||
current = state
|
||||
end)
|
||||
|
||||
condition(true)
|
||||
expect(reruns).to.equal(1)
|
||||
expect(current).to.equal(0)
|
||||
|
||||
a(1)
|
||||
b(2)
|
||||
expect(reruns).to.equal(2)
|
||||
expect(current).to.equal(1)
|
||||
|
||||
condition(false)
|
||||
expect(reruns).to.equal(3)
|
||||
expect(current).to.equal(2)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
--!nocheck
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)
|
||||
|
||||
-- Start Charm in debug mode
|
||||
_G.__DEV__ = true
|
||||
require(ReplicatedStorage.DevPackages.Charm)
|
||||
require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
_G.__DEV__ = false
|
||||
|
||||
TestEZ.TestBootstrap:run({ script.Parent }, TestEZ.Reporters.TextReporter)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
afterEach(function()
|
||||
table.clear(store.listeners :: any)
|
||||
table.clear(store.capturing.stack)
|
||||
store.capturing.index = 0
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local ReactCharm = require(ReplicatedStorage.DevPackages.ReactCharm)
|
||||
local useAtom = ReactCharm.useAtom
|
||||
local renderHook = require(script.Parent.Parent.utils.renderHook)
|
||||
|
||||
return function()
|
||||
it("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(state)
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
end)
|
||||
|
||||
it("accepts a selector", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(function()
|
||||
return 2 * state()
|
||||
end)
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(2)
|
||||
end)
|
||||
|
||||
it("tracks dependencies", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(state, { state })
|
||||
end)
|
||||
expect(hook.result).to.equal(0)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
state = atom(0)
|
||||
hook.rerender()
|
||||
expect(hook.result).to.equal(0)
|
||||
state(1)
|
||||
expect(hook.result).to.equal(1)
|
||||
end)
|
||||
end
|
||||
33
tests/run.server.luau
Normal file
33
tests/run.server.luau
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
local Charm = require("../packages/charm/src")
|
||||
local test = require("./test")
|
||||
|
||||
Charm.flags.strict = true
|
||||
Charm.flags.frozen = true
|
||||
|
||||
local describeInRoblox = test.describeIf(function()
|
||||
return not not game
|
||||
end)
|
||||
|
||||
test.describe("charm", function()
|
||||
require("../packages/charm/test")
|
||||
end)
|
||||
|
||||
test.describe("charm-sync", function()
|
||||
require("../packages/charm-sync/test")
|
||||
end)
|
||||
|
||||
test.describe("deep-charm", function()
|
||||
require("../packages/deep-charm/test")
|
||||
end)
|
||||
|
||||
describeInRoblox("react-charm", function()
|
||||
require("../packages/react-charm/test")
|
||||
end)
|
||||
|
||||
describeInRoblox("vide-charm", function()
|
||||
require("../packages/vide-charm/test")
|
||||
end)
|
||||
|
||||
test.finish()
|
||||
|
||||
return {}
|
||||
1
tests/test
Submodule
1
tests/test
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 7ef2b88191af884d543e7392d50c7e2adc484e32
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
--[=[
|
||||
Trigger a garbage collection cycle by allocating a large amount of junk memory
|
||||
and then waiting for the next frame.
|
||||
]=]
|
||||
local function collect()
|
||||
for _ = 1, 1e4 do
|
||||
local _ = table.create(1e3)
|
||||
end
|
||||
task.wait()
|
||||
end
|
||||
|
||||
return collect
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
--[=[
|
||||
Counts the number of elements in a table.
|
||||
|
||||
@param object The table to count.
|
||||
@return The number of elements in the table.
|
||||
]=]
|
||||
local function count(object: any): number
|
||||
local count = 0
|
||||
for _ in next, object do
|
||||
count += 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
return count
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||
|
||||
local React = require(ReplicatedStorage.DevPackages.React)
|
||||
local ReactRoblox = require(ReplicatedStorage.DevPackages.ReactRoblox)
|
||||
|
||||
type RenderedHook<Props, Result> = {
|
||||
result: Result,
|
||||
rerender: (Props?) -> (),
|
||||
unmount: () -> (),
|
||||
}
|
||||
|
||||
local function renderHook<Props, Result>(callback: (Props) -> Result, props: Props?): RenderedHook<Props, Result>
|
||||
local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
|
||||
local hook = {}
|
||||
|
||||
local function Component(props)
|
||||
hook.result = callback(props)
|
||||
end
|
||||
|
||||
function hook.rerender(newProps)
|
||||
props = newProps or props
|
||||
ReactRoblox.act(function()
|
||||
root:render(React.createElement(Component, props))
|
||||
end)
|
||||
end
|
||||
|
||||
function hook.unmount()
|
||||
ReactRoblox.act(function()
|
||||
root:unmount()
|
||||
end)
|
||||
end
|
||||
|
||||
ReactRoblox.act(function()
|
||||
hook.rerender(props)
|
||||
end)
|
||||
|
||||
return hook
|
||||
end
|
||||
|
||||
return renderHook
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local Vide = require(ReplicatedStorage.DevPackages.Vide)
|
||||
local root = Vide.root
|
||||
local VideCharm = require(ReplicatedStorage.DevPackages.VideCharm)
|
||||
local useAtom = VideCharm.useAtom
|
||||
|
||||
return function()
|
||||
it("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
local source
|
||||
local cleanup = root(function()
|
||||
source = useAtom(state)
|
||||
end)
|
||||
expect(source()).to.equal(0)
|
||||
state(1)
|
||||
expect(source()).to.equal(1)
|
||||
cleanup()
|
||||
end)
|
||||
|
||||
it("accepts a selector", function()
|
||||
local state = atom(0)
|
||||
local source
|
||||
local cleanup = root(function()
|
||||
source = useAtom(function()
|
||||
return 2 * state()
|
||||
end)
|
||||
end)
|
||||
expect(source()).to.equal(0)
|
||||
state(1)
|
||||
expect(source()).to.equal(2)
|
||||
cleanup()
|
||||
end)
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue