mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: improve structure and alien-signals integration
This commit is contained in:
parent
a9c60c701f
commit
95476e93ec
107 changed files with 3660 additions and 815 deletions
6
packages/charm-sync/Charm.luau
Normal file
6
packages/charm-sync/Charm.luau
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
local Charm = require(script.Parent.Parent.charm.src)
|
||||
|
||||
export type Atom<T = any> = Charm.Atom<T>
|
||||
export type Selector<T = any> = Charm.Selector<T>
|
||||
|
||||
return Charm
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "CharmSync",
|
||||
"name": "charm-sync",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
|
|
|
|||
27
packages/charm-sync/package.json
Normal file
27
packages/charm-sync/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@rbxts/charm-sync",
|
||||
"version": "0.3.0",
|
||||
"description": "Simple server-to-client sync for Charm",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"*.luau",
|
||||
"!wally.toml",
|
||||
"!wally.lock",
|
||||
"!default.project.json",
|
||||
"!**/__tests__"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/littensy/charm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local CharmSync = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local CharmSync = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local flatten = CharmSync.flatten
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
local HttpService = game:GetService("HttpService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local CharmSync = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local patch = CharmSync.patch
|
||||
local test = suite.test
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local CharmSync = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
type SyncPayload = CharmSync.SyncPayload
|
||||
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("charm-sync", function()
|
||||
if not game then
|
||||
return suite.skip()
|
||||
end
|
||||
suite.describe("client", function()
|
||||
require("./client.spec")
|
||||
end)
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local CharmSync = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local validate = CharmSync.validate
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
local Charm = require(script.Parent.Parent.Charm)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
local Charm = require("../Charm")
|
||||
local patch = require("./patch")
|
||||
local types = require("./types")
|
||||
|
||||
type AtomMap = types.AtomMap
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
|
@ -36,7 +36,7 @@ local function client(options: ClientOptions): ClientSyncer
|
|||
local function hydrate(state: { [string | number]: any })
|
||||
hydrated = true
|
||||
|
||||
for key, value in next, state do
|
||||
for key, value in state do
|
||||
local atom = atoms[key]
|
||||
atom(value)
|
||||
end
|
||||
|
|
@ -45,13 +45,13 @@ local function client(options: ClientOptions): ClientSyncer
|
|||
local function apply(data: { [string | number]: any })
|
||||
local target = {}
|
||||
|
||||
for key, atom in next, atoms do
|
||||
for key, atom in atoms do
|
||||
target[key] = atom()
|
||||
end
|
||||
|
||||
target = patch.apply(target, data)
|
||||
|
||||
for key, atom in next, atoms do
|
||||
for key, atom in atoms do
|
||||
atom(target[key])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
local types = require(script.Parent.types)
|
||||
local types = require("./types")
|
||||
|
||||
type AtomMap = types.AtomMap
|
||||
|
||||
|
|
|
|||
1
packages/charm-sync/src/index.d.ts
vendored
Normal file
1
packages/charm-sync/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
local client = require(script.client)
|
||||
local flatten = require(script.flatten)
|
||||
local patch = require(script.patch)
|
||||
local server = require(script.server)
|
||||
local types = require(script.types)
|
||||
local validate = require(script.validate)
|
||||
local client = require("@self/client")
|
||||
local flatten = require("@self/flatten")
|
||||
local patch = require("@self/patch")
|
||||
local server = require("@self/server")
|
||||
local types = require("@self/types")
|
||||
local validate = require("@self/validate")
|
||||
|
||||
export type SyncPayload = types.SyncPayload
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
--- Schedules a function to be called at a fixed interval of `seconds`.
|
||||
---
|
||||
--- @param callback The function to call.
|
||||
|
|
@ -12,7 +10,7 @@ local function interval(callback: () -> (), seconds: number): () -> ()
|
|||
|
||||
local clock = 0
|
||||
|
||||
local connection = RunService.Heartbeat:Connect(function(delta)
|
||||
local connection = game:GetService("RunService").Heartbeat:Connect(function(delta)
|
||||
clock += delta
|
||||
if clock >= seconds then
|
||||
clock = 0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
local validate = require(script.Parent.validate)
|
||||
|
||||
local __DEV__ = _G.__DEV__
|
||||
local Charm = require("../Charm")
|
||||
local validate = require("./validate")
|
||||
|
||||
--- A special symbol that denotes the absence of a value. Used to represent
|
||||
--- deleted values in patches.
|
||||
|
|
@ -64,7 +63,7 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serial
|
|||
patches = stringifySparseArray(patches)
|
||||
end
|
||||
|
||||
if serialize and __DEV__ then
|
||||
if serialize and Charm.strict.enabled then
|
||||
for key, value in next, patches do
|
||||
validate(value, key)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local Charm = require(script.Parent.Parent.Charm)
|
||||
local interval = require(script.Parent.interval)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
local Charm = require("../Charm")
|
||||
local interval = require("./interval")
|
||||
local patch = require("./patch")
|
||||
local types = require("./types")
|
||||
|
||||
type AtomMap = types.AtomMap
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
|
@ -64,7 +62,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
local function createSnapshot()
|
||||
local snapshot = {}
|
||||
|
||||
for key, atom in next, atoms do
|
||||
for key, atom in atoms do
|
||||
snapshot[key] = atom()
|
||||
end
|
||||
|
||||
|
|
@ -110,7 +108,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
|
||||
-- Populate the snapshot with the initial state of each atom.
|
||||
-- Subscribe to each atom and update the state when it changes.
|
||||
for key, atom in next, atoms do
|
||||
for key, atom in atoms do
|
||||
subscriptions[key] = Charm.subscribe(atom, function(current, previous)
|
||||
pushSnapshot(key, current, previous)
|
||||
changed = true
|
||||
|
|
@ -125,7 +123,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
local payloads: { SyncPayload } = {}
|
||||
local lastSnapshot
|
||||
|
||||
for index, snapshot in next, snapshots do
|
||||
for index, snapshot in snapshots do
|
||||
lastSnapshot = snapshot
|
||||
|
||||
if index == 1 then
|
||||
|
|
@ -141,7 +139,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
snapshots = { lastSnapshot }
|
||||
changed = false
|
||||
|
||||
for _, player in next, Players:GetPlayers() do
|
||||
for _, player in game:GetService("Players"):GetPlayers() do
|
||||
callback(player, unpack(payloads))
|
||||
end
|
||||
end, syncRate)
|
||||
|
|
@ -149,7 +147,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
return function()
|
||||
disconnect()
|
||||
|
||||
for _, unsubscribe in next, subscriptions do
|
||||
for _, unsubscribe in subscriptions do
|
||||
unsubscribe()
|
||||
end
|
||||
end
|
||||
|
|
@ -164,7 +162,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
|
||||
local payloads: { SyncPayload } = {}
|
||||
|
||||
for index, snapshot in next, snapshots do
|
||||
for index, snapshot in snapshots do
|
||||
if index == 1 then
|
||||
continue
|
||||
end
|
||||
|
|
@ -194,7 +192,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
sync = callback
|
||||
|
||||
-- Subscribe to each atom and update the state when one changes.
|
||||
for key, atom in next, atoms do
|
||||
for key, atom in atoms do
|
||||
subscriptions[key] = Charm.subscribe(atom, function(state)
|
||||
currentSnapshot[key] = state
|
||||
changed = true
|
||||
|
|
@ -214,7 +212,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
previousSnapshot = table.clone(currentSnapshot)
|
||||
changed = false
|
||||
|
||||
for _, player in next, Players:GetPlayers() do
|
||||
for _, player in game:GetService("Players"):GetPlayers() do
|
||||
callback(player, payload)
|
||||
end
|
||||
end, syncRate)
|
||||
|
|
@ -222,7 +220,7 @@ local function server(options: ServerOptions): ServerSyncer
|
|||
return function()
|
||||
disconnect()
|
||||
|
||||
for _, unsubscribe in next, subscriptions do
|
||||
for _, unsubscribe in subscriptions do
|
||||
unsubscribe()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
local Charm = require(script.Parent.Parent.Charm)
|
||||
local Charm = require("../Charm")
|
||||
|
||||
type Atom<T> = Charm.Atom<T>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ name = "littensy/charm-sync"
|
|||
version = "0.3.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["tests"]
|
||||
exclude = [
|
||||
"node_modules",
|
||||
"src/__tests__",
|
||||
"src/index.d.ts",
|
||||
"package.json",
|
||||
"Charm.luau",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Charm",
|
||||
"name": "charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
|
|
|
|||
24
packages/charm/package.json
Normal file
24
packages/charm/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@rbxts/charm",
|
||||
"version": "0.10.0",
|
||||
"description": "Atomic state management based on alien-signals",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"*.luau",
|
||||
"!wally.toml",
|
||||
"!wally.lock",
|
||||
"!default.project.json",
|
||||
"!**/__tests__"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/littensy/charm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local signal = signals.signal
|
||||
local computed = core.computed
|
||||
local atom = core.atom
|
||||
local test = suite.test
|
||||
|
||||
test("should correctly propagate changes through computed signals", function()
|
||||
local src, setSrc = signal(0)
|
||||
test("should correctly propagate changes through computed core", function()
|
||||
local src = atom(0)
|
||||
local c1 = computed(function()
|
||||
return src() % 2
|
||||
end)
|
||||
|
|
@ -18,15 +18,15 @@ test("should correctly propagate changes through computed signals", function()
|
|||
end)
|
||||
|
||||
c3()
|
||||
setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
c2() -- c1 -> none, c2 -> none
|
||||
setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
src(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
|
||||
assert(c3() == 1, "bad final value")
|
||||
end)
|
||||
|
||||
test("should propagate updated source value through chained computations", function()
|
||||
local src, setSrc = signal(0)
|
||||
local src = atom(0)
|
||||
local a = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
|
@ -41,12 +41,12 @@ test("should propagate updated source value through chained computations", funct
|
|||
end)
|
||||
|
||||
assert(d() == 0, "bad initial value")
|
||||
setSrc(2)
|
||||
src(2)
|
||||
assert(d() == 2, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a, setA = signal(false)
|
||||
local a = atom(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -60,22 +60,22 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
end)
|
||||
|
||||
assert(d() == false, "bad initial value")
|
||||
setA(true)
|
||||
a(true)
|
||||
assert(d() == true, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should not update if the signal value is reverted", function()
|
||||
test("should not update if the atom value is reverted", function()
|
||||
local times = 0
|
||||
|
||||
local src, setSrc = signal(0)
|
||||
local src = atom(0)
|
||||
local c1 = computed(function()
|
||||
times += 1
|
||||
return src()
|
||||
end)
|
||||
c1()
|
||||
assert(times == 1, "computed did not run")
|
||||
setSrc(1)
|
||||
setSrc(0)
|
||||
src(1)
|
||||
src(0)
|
||||
c1()
|
||||
assert(times == 1, "computed ran when it should not")
|
||||
end)
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local endBatch = signals.endBatch
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local signal = signals.signal
|
||||
local startBatch = signals.startBatch
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local endBatch = core.endBatch
|
||||
local setCurrentSub = core.setCurrentSub
|
||||
local onCleanup = core.onCleanup
|
||||
local atom = core.atom
|
||||
local startBatch = core.startBatch
|
||||
local test = suite.test
|
||||
|
||||
test("should clear subscriptions when untracked by all subscribers", function()
|
||||
local bRunTimes = 0
|
||||
|
||||
local a, setA = signal(1)
|
||||
local a = atom(1)
|
||||
local b = computed(function()
|
||||
bRunTimes += 1
|
||||
return a() * 2
|
||||
|
|
@ -24,15 +24,15 @@ test("should clear subscriptions when untracked by all subscribers", function()
|
|||
end)
|
||||
|
||||
assert(bRunTimes == 1, "should run initially")
|
||||
setA(2)
|
||||
a(2)
|
||||
assert(bRunTimes == 2, "should run after change")
|
||||
stopEffect()
|
||||
setA(3)
|
||||
a(3)
|
||||
assert(bRunTimes == 2, "should not run after stop")
|
||||
end)
|
||||
|
||||
test("should not run untracked inner effect", function()
|
||||
local a, setA = signal(3)
|
||||
local a = atom(3)
|
||||
local b = computed(function()
|
||||
return a() > 0
|
||||
end)
|
||||
|
|
@ -47,14 +47,14 @@ test("should not run untracked inner effect", function()
|
|||
end
|
||||
end)
|
||||
|
||||
setA(2)
|
||||
setA(1)
|
||||
setA(0)
|
||||
a(2)
|
||||
a(1)
|
||||
a(0)
|
||||
end)
|
||||
|
||||
test("should run outer effect first", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(1)
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
|
||||
effect(function()
|
||||
if a() > 0 then
|
||||
|
|
@ -68,13 +68,13 @@ test("should run outer effect first", function()
|
|||
end)
|
||||
|
||||
startBatch()
|
||||
setB(0)
|
||||
setA(0)
|
||||
b(0)
|
||||
a(0)
|
||||
endBatch()
|
||||
end)
|
||||
|
||||
test("should not trigger inner effect when resolve maybe dirty", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
return a() % 2
|
||||
end)
|
||||
|
|
@ -91,12 +91,12 @@ test("should not trigger inner effect when resolve maybe dirty", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
setA(2)
|
||||
a(2)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence", function()
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local c = computed(function()
|
||||
return a() - b()
|
||||
end)
|
||||
|
|
@ -120,16 +120,16 @@ test("should trigger inner effects in sequence", function()
|
|||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
setB(1)
|
||||
setA(1)
|
||||
b(1)
|
||||
a(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence in effect scope", function()
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effectScope(function()
|
||||
|
|
@ -148,8 +148,8 @@ test("should trigger inner effects in sequence in effect scope", function()
|
|||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
setB(1)
|
||||
setA(1)
|
||||
b(1)
|
||||
a(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
|
|
@ -168,13 +168,13 @@ test("should custom effect support batch", function()
|
|||
end
|
||||
|
||||
local logs: { string } = {}
|
||||
local a = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
|
||||
local aa = computed(function()
|
||||
table.insert(logs, "aa-0")
|
||||
if a() == 0 then
|
||||
setB(1)
|
||||
b(1)
|
||||
end
|
||||
table.insert(logs, "aa-1")
|
||||
return nil
|
||||
|
|
@ -196,8 +196,8 @@ test("should custom effect support batch", function()
|
|||
end)
|
||||
|
||||
test("should duplicate subscribers do not affect the notify order", function()
|
||||
local src1, setSrc1 = signal(0)
|
||||
local src2, setSrc2 = signal(0)
|
||||
local src1 = atom(0)
|
||||
local src2 = atom(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
|
|
@ -215,17 +215,17 @@ test("should duplicate subscribers do not affect the notify order", function()
|
|||
table.insert(order, "b")
|
||||
src1()
|
||||
end)
|
||||
setSrc2(1) -- src1.subs: a -> b -> a
|
||||
src2(1) -- src1.subs: a -> b -> a
|
||||
|
||||
table.clear(order)
|
||||
setSrc1(src1() + 1)
|
||||
src1(src1() + 1)
|
||||
|
||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should handle side effect with inner effects", function()
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
|
|
@ -240,14 +240,14 @@ test("should handle side effect with inner effects", function()
|
|||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
setB(1)
|
||||
setA(1)
|
||||
b(1)
|
||||
a(1)
|
||||
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a, setA = signal(false)
|
||||
local a = atom(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -267,27 +267,27 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
triggers += 1
|
||||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
setA(true)
|
||||
a(true)
|
||||
assert(triggers == 2, "did not update indirectly")
|
||||
end)
|
||||
|
||||
test("should run cleanups in sequence", function()
|
||||
local order = {}
|
||||
|
||||
local a, setA = signal(false)
|
||||
local a = atom(false)
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
table.insert(order, "a")
|
||||
end)
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
table.insert(order, "b")
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(table.concat(order) == "", "should not cleanup immediately")
|
||||
|
||||
setA(true)
|
||||
a(true)
|
||||
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
|
|
@ -295,45 +295,45 @@ test("should run cleanups in sequence", function()
|
|||
assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
setA(false)
|
||||
a(false)
|
||||
assert(table.concat(order) == "", "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run inner effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local src, setSrc = signal(false)
|
||||
local src = atom(false)
|
||||
local stopEffect = effect(function()
|
||||
src()
|
||||
effect(function()
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
end)
|
||||
effect(function()
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
setSrc(true)
|
||||
src(true)
|
||||
assert(cleanups == 2, "should cleanup after outer update")
|
||||
stopEffect()
|
||||
assert(cleanups == 4, "should cleanup after outer dispose")
|
||||
setSrc(false)
|
||||
src(false)
|
||||
assert(cleanups == 4, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run cleanups outside any evaluation context", function()
|
||||
local calls = 0
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local c = computed(function()
|
||||
if a() == 0 then
|
||||
effect(function()
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
b()
|
||||
end)
|
||||
end)
|
||||
|
|
@ -348,11 +348,11 @@ test("should run cleanups outside any evaluation context", function()
|
|||
assert(calls == 1, "should run once")
|
||||
|
||||
calls = 0
|
||||
setA(1)
|
||||
a(1)
|
||||
assert(calls == 1, "should run once after a update")
|
||||
|
||||
calls = 0
|
||||
setB(1)
|
||||
b(1)
|
||||
assert(calls == 0, "should not run after b update")
|
||||
end)
|
||||
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local signal = signals.signal
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local atom = core.atom
|
||||
local onCleanup = core.onCleanup
|
||||
local test = suite.test
|
||||
|
||||
test("should not trigger after stop", function()
|
||||
local count, setCount = signal(1)
|
||||
local count = atom(1)
|
||||
|
||||
local triggers = 0
|
||||
|
||||
|
|
@ -20,14 +19,14 @@ test("should not trigger after stop", function()
|
|||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
|
||||
setCount(2)
|
||||
count(2)
|
||||
assert(triggers == 2, "did not run after update")
|
||||
end)
|
||||
|
||||
setCount(3)
|
||||
count(3)
|
||||
assert(triggers == 3, "did not run after update outside scope")
|
||||
stopScope()
|
||||
setCount(4)
|
||||
count(4)
|
||||
assert(triggers == 3, "ran after stop")
|
||||
end)
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ test("should run indirect scope cleanup", function()
|
|||
local cleanup
|
||||
local stopScope = effectScope(function()
|
||||
cleanup = effectScope(function()
|
||||
onScopeDispose(function()
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
end)
|
||||
|
|
@ -53,22 +52,22 @@ end)
|
|||
test("should run indirect effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a, setA = signal(false)
|
||||
local a = atom(false)
|
||||
local stopScope = effectScope(function()
|
||||
effect(function()
|
||||
a()
|
||||
onEffectCleanup(function()
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
setA(true)
|
||||
a(true)
|
||||
assert(cleanups == 1, "should cleanup once")
|
||||
stopScope()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
setA(false)
|
||||
a(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local signal = signals.signal
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local atom = core.atom
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local test = suite.test
|
||||
|
||||
--- Creates a weak reference to a value.
|
||||
|
|
@ -23,13 +23,13 @@ local function gc()
|
|||
end
|
||||
end
|
||||
|
||||
--- In alien-signals, a computed value unlinks from dependencies when its last
|
||||
--- In alien-core, 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.
|
||||
---
|
||||
--- 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
|
||||
--- immediately closing it, causing alien-core to unlink the computed value
|
||||
--- and allow it to be garbage collected.
|
||||
local function release(getter: () -> ())
|
||||
effectScope(function()
|
||||
|
|
@ -39,23 +39,23 @@ end
|
|||
|
||||
test("should release computed cache", function()
|
||||
local srcRef = weakRef({})
|
||||
local src, setSrc = signal(srcRef())
|
||||
local src = atom(srcRef())
|
||||
|
||||
local c = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
||||
c() -- cache src value
|
||||
setSrc(nil) -- release value
|
||||
src(nil) -- release value
|
||||
c = release(c) -- release computed
|
||||
|
||||
gc()
|
||||
assert(srcRef() == nil, "should release computed cache")
|
||||
end)
|
||||
|
||||
test("should not release effect for signal", function()
|
||||
test("should not release effect for atom", function()
|
||||
local effectCalls = 0
|
||||
local src, setSrc = signal(0)
|
||||
local src = atom(0)
|
||||
|
||||
effect(function()
|
||||
effectCalls += 1
|
||||
|
|
@ -65,7 +65,7 @@ test("should not release effect for signal", function()
|
|||
assert(effectCalls == 1, "should call effect once")
|
||||
|
||||
gc()
|
||||
setSrc(src() + 1)
|
||||
src(src() + 1)
|
||||
assert(effectCalls == 2, "should call effect again")
|
||||
end)
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
local signal = signals.signal
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local setCurrentSub = core.setCurrentSub
|
||||
local atom = core.atom
|
||||
local test = suite.test
|
||||
|
||||
type ReactionOptions<T = unknown> = {
|
||||
|
|
@ -90,7 +90,7 @@ local function reaction<T>(
|
|||
end
|
||||
|
||||
test("#48", function()
|
||||
local source, setSource = signal(0)
|
||||
local source = atom(0)
|
||||
local disposeInner: () -> ()
|
||||
|
||||
reaction(function()
|
||||
|
|
@ -105,9 +105,9 @@ test("#48", function()
|
|||
end
|
||||
end)
|
||||
|
||||
setSource(1)
|
||||
setSource(2)
|
||||
setSource(3)
|
||||
source(1)
|
||||
source(2)
|
||||
source(3)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local signal = signals.signal
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local atom = core.atom
|
||||
local describe = suite.describe
|
||||
local test = suite.test
|
||||
|
||||
--[[
|
||||
Tests adopted with thanks from preact-signals implementation at
|
||||
https://github.com/preactjs/signals/blob/main/packages/core/test/signal.test.tsx
|
||||
Tests adopted with thanks from preact-core implementation at
|
||||
https://github.com/preactjs/core/blob/main/packages/core/test/atom.test.tsx
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ describe("graph updates", function()
|
|||
-- C
|
||||
-- |
|
||||
-- D
|
||||
local a, setA = signal(2)
|
||||
local a = atom(2)
|
||||
|
||||
local b = computed(function()
|
||||
return a() - 1
|
||||
|
|
@ -64,12 +64,12 @@ describe("graph updates", function()
|
|||
assert(computeCalls == 1, "compute should be called once")
|
||||
computeCalls = 0
|
||||
|
||||
setA(4)
|
||||
a(4)
|
||||
d()
|
||||
assert(computeCalls == 1, "compute should drop update")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (diamond graph)", function()
|
||||
test("should only update every atom once (diamond graph)", function()
|
||||
-- In this scenario "D" should only update once when "A" receives
|
||||
-- an update. This is sometimes referred to as the "diamond" scenario.
|
||||
-- A
|
||||
|
|
@ -78,7 +78,7 @@ describe("graph updates", function()
|
|||
-- \ /
|
||||
-- D
|
||||
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -96,12 +96,12 @@ describe("graph updates", function()
|
|||
assert(d() == "a a", "d should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (diamond graph + tail)", function()
|
||||
test("should only update every atom once (diamond graph + tail)", function()
|
||||
-- "E" will be likely updated twice if our mark+sweep logic is buggy.
|
||||
-- A
|
||||
-- / \
|
||||
|
|
@ -111,7 +111,7 @@ describe("graph updates", function()
|
|||
-- |
|
||||
-- E
|
||||
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -133,7 +133,7 @@ describe("graph updates", function()
|
|||
assert(e() == "a a", "e should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -141,7 +141,7 @@ describe("graph updates", function()
|
|||
test("should bail out if result is the same", function()
|
||||
-- Bail out if value of "B" never changes
|
||||
-- A->B->C
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "foo"
|
||||
|
|
@ -157,12 +157,12 @@ describe("graph updates", function()
|
|||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should bail out")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (jagged diamond graph + tails)", function()
|
||||
test("should only update every atom once (jagged diamond graph + tails)", function()
|
||||
-- "F" and "G" will be likely updated twice if our mark+sweep logic is buggy.
|
||||
-- A
|
||||
-- / \
|
||||
|
|
@ -173,7 +173,7 @@ describe("graph updates", function()
|
|||
-- E
|
||||
-- / \
|
||||
-- F G
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -222,7 +222,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
setA("b")
|
||||
a("b")
|
||||
|
||||
assert(e() == "b b", "e should be 'b b'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -237,7 +237,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
setA("c")
|
||||
a("c")
|
||||
|
||||
assert(e() == "c c", "e should be 'c c'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -254,11 +254,11 @@ describe("graph updates", function()
|
|||
assert(eSpyTime < gSpyTime, "e should be called before g")
|
||||
end)
|
||||
|
||||
test("should only subscribe to signals listened to", function()
|
||||
test("should only subscribe to core listened to", function()
|
||||
-- *A
|
||||
-- / \
|
||||
-- *B C <- we don't listen to C
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -273,12 +273,12 @@ describe("graph updates", function()
|
|||
assert(b() == "a", "b should be 'a'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(b() == "aa", "b should be 'aa'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
end)
|
||||
|
||||
test("should only subscribe to signals listened to II", function()
|
||||
test("should only subscribe to core listened to II", function()
|
||||
-- Here both "B" and "C" are active in the beginning, but
|
||||
-- "B" becomes inactive later. At that point it should
|
||||
-- not receive any updates anymore.
|
||||
|
|
@ -287,7 +287,7 @@ describe("graph updates", function()
|
|||
-- *B D <- we don't listen to C
|
||||
-- |
|
||||
-- *C
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local spyBCalls = 0
|
||||
local spyB = function()
|
||||
spyBCalls += 1
|
||||
|
|
@ -318,7 +318,7 @@ describe("graph updates", function()
|
|||
spyCCalls = 0
|
||||
unsub()
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
|
||||
assert(spyBCalls == 0, "b should not run")
|
||||
assert(spyCCalls == 0, "c should not run")
|
||||
|
|
@ -335,7 +335,7 @@ describe("graph updates", function()
|
|||
-- B *C <- returns same value every time
|
||||
-- \ /
|
||||
-- D
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -353,7 +353,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a c", "d should be 'a c'")
|
||||
spyResult = nil
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
d()
|
||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||
end)
|
||||
|
|
@ -367,7 +367,7 @@ describe("graph updates", function()
|
|||
-- B *C *D
|
||||
-- \ | /
|
||||
-- E
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -389,13 +389,13 @@ describe("graph updates", function()
|
|||
assert(e() == "a c d", "e should be 'a c d'")
|
||||
spyResult = nil
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
e()
|
||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||
end)
|
||||
|
||||
test("should support lazy branches", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -404,10 +404,10 @@ describe("graph updates", function()
|
|||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
setA(1)
|
||||
a(1)
|
||||
assert(c() == 1, "c should be 1")
|
||||
|
||||
setA(0)
|
||||
a(0)
|
||||
assert(c() == 0, "c should be 0 again")
|
||||
end)
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ describe("graph updates", function()
|
|||
-- *B *C
|
||||
-- \ /
|
||||
-- D
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "b"
|
||||
|
|
@ -438,14 +438,14 @@ describe("graph updates", function()
|
|||
assert(d() == "b c", "d should be 'b c'")
|
||||
spyCalls = 0
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(spyCalls == 0, "d should not run")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("error handling", function()
|
||||
test("should keep graph consistent on errors during activation", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
error("fail")
|
||||
end)
|
||||
|
|
@ -459,12 +459,12 @@ describe("error handling", function()
|
|||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
setA(1)
|
||||
a(1)
|
||||
assert(c() == 1, "c should recover and return 1")
|
||||
end)
|
||||
|
||||
test("should keep graph consistent on errors in computeds", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
if a() == 1 then
|
||||
error("fail")
|
||||
|
|
@ -477,14 +477,14 @@ describe("error handling", function()
|
|||
|
||||
assert(c() == 0, "c should be 0")
|
||||
|
||||
setA(1)
|
||||
a(1)
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
setA(2)
|
||||
a(2)
|
||||
assert(c() == 2, "c should recover and return 2")
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
local signal = signals.signal
|
||||
local computed = core.computed
|
||||
local setCurrentSub = core.setCurrentSub
|
||||
local atom = core.atom
|
||||
local test = suite.test
|
||||
|
||||
test("should pause tracking", function()
|
||||
local src, setSrc = signal(0)
|
||||
local src = atom(0)
|
||||
local c = computed(function()
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local value = src()
|
||||
|
|
@ -16,7 +16,7 @@ test("should pause tracking", function()
|
|||
end)
|
||||
assert(c() == 0, "should not track src")
|
||||
|
||||
setSrc(1)
|
||||
src(1)
|
||||
assert(c() == 0, "should not track src")
|
||||
end)
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src")
|
||||
local suite = require("@test/suite")
|
||||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local listen = Charm.listen
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src")
|
||||
local suite = require("@test/suite")
|
||||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local mapped = Charm.mapped
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src")
|
||||
local suite = require("@test/suite")
|
||||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
45
packages/charm/src/__tests__/spec.luau
Normal file
45
packages/charm/src/__tests__/spec.luau
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("core", function()
|
||||
suite.describe("computed", function()
|
||||
require("./core/computed.spec")
|
||||
end)
|
||||
suite.describe("effect", function()
|
||||
require("./core/effect.spec")
|
||||
end)
|
||||
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("charm", function()
|
||||
suite.describe("listen", function()
|
||||
require("./listen.spec")
|
||||
end)
|
||||
suite.describe("mapped", function()
|
||||
require("./mapped.spec")
|
||||
end)
|
||||
suite.describe("observe", function()
|
||||
require("./observe.spec")
|
||||
end)
|
||||
suite.describe("subscribe", function()
|
||||
require("./subscribe.spec")
|
||||
end)
|
||||
suite.describe("untracked", function()
|
||||
require("./untracked.spec")
|
||||
end)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src")
|
||||
local suite = require("@test/suite")
|
||||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
type Atom<T> = Charm.Atom<T>
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src")
|
||||
local suite = require("@test/suite")
|
||||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
local types = require("./types")
|
||||
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Getter<T> = types.Getter<T>
|
||||
type Setter<T> = types.Setter<T>
|
||||
type Update<T> = types.Update<T>
|
||||
|
||||
local signal = signals.signal
|
||||
|
||||
type Equals<T> = (current: T, incoming: T) -> boolean
|
||||
|
||||
local function createAtom<T>(getter: Getter<T>, setter: Setter<T>): Atom<T>
|
||||
local function atom(...: Update<T>): T
|
||||
return if select("#", ...) == 0 then getter() else setter((...))
|
||||
end
|
||||
|
||||
return atom
|
||||
end
|
||||
|
||||
local function createAtomWithEqualityCheck<T>(getter: Getter<T>, setter: Setter<T>, equals: Equals<T>): Atom<T>
|
||||
local function atom(...: Update<T>): T
|
||||
if select("#", ...) == 0 then
|
||||
return getter()
|
||||
end
|
||||
|
||||
local nextState = (...)
|
||||
|
||||
return setter(function(currentState): T
|
||||
local nextState = if type(nextState) == "function" then nextState(currentState) else nextState
|
||||
return if equals(currentState, nextState) then currentState else nextState
|
||||
end)
|
||||
end
|
||||
|
||||
return atom
|
||||
end
|
||||
|
||||
local function atom<T>(state: T, equals: Equals<T>?): Atom<T>
|
||||
local getter, setter = signal(state)
|
||||
|
||||
if not equals then
|
||||
return createAtom(getter, setter)
|
||||
else
|
||||
return createAtomWithEqualityCheck(getter, setter, equals)
|
||||
end
|
||||
end
|
||||
|
||||
return atom
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
|
||||
type ReactiveNode = signals.ReactiveNode
|
||||
|
||||
local startBatch = signals.startBatch
|
||||
local endBatch = signals.endBatch
|
||||
local startBatch = core.startBatch
|
||||
local endBatch = core.endBatch
|
||||
|
||||
local function handleBatched(ok: boolean, ...)
|
||||
endBatch()
|
||||
if ok then
|
||||
return ...
|
||||
else
|
||||
error(...)
|
||||
error((...))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
--[[
|
||||
alien-signals 2.0.1 implementation adopted from
|
||||
alien-signals 2.0.5 implementation adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
||||
|
|
@ -26,8 +26,9 @@
|
|||
SOFTWARE.
|
||||
]]
|
||||
|
||||
local noYield = require("./noYield")
|
||||
local strict = require("./strict")
|
||||
local system = require("./system")
|
||||
local utils = require("./utils")
|
||||
|
||||
export type ReactiveFlags = system.ReactiveFlags
|
||||
export type ReactiveNode = system.ReactiveNode
|
||||
|
|
@ -35,6 +36,7 @@ export type Link = system.Link
|
|||
|
||||
local ReactiveFlags = system.ReactiveFlags
|
||||
local createReactiveSystem = system.createReactiveSystem
|
||||
local noYield = utils.noYield
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
|
|
@ -48,23 +50,22 @@ type EffectScope = ReactiveNode & {
|
|||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (),
|
||||
fn: () -> ...(() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
value: T?,
|
||||
getter: (previousValue: T?) -> T,
|
||||
equals: ((T, T) -> boolean)?,
|
||||
}
|
||||
|
||||
type Signal<T = any> = ReactiveNode & {
|
||||
type Atom<T = any> = ReactiveNode & {
|
||||
previousValue: T,
|
||||
value: T,
|
||||
equals: ((T, T) -> boolean)?,
|
||||
}
|
||||
|
||||
local pauseStack: { ReactiveNode? } = {}
|
||||
local queuedEffects: { Effect | EffectScope? } = {}
|
||||
|
||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||
local propagate: (link: Link) -> ()
|
||||
|
|
@ -74,9 +75,10 @@ local startTracking: (sub: ReactiveNode) -> ()
|
|||
local shallowPropagate: (link: Link) -> ()
|
||||
|
||||
local batchDepth = 0
|
||||
|
||||
local notifyIndex = 0
|
||||
local queuedEffects: { Effect | EffectScope? } = {}
|
||||
local queuedEffectsLength = 0
|
||||
local notifyIndex = 0
|
||||
local activeSubTracking = true
|
||||
local activeSub: ReactiveNode?
|
||||
local activeScope: Effect | EffectScope?
|
||||
|
||||
|
|
@ -100,50 +102,58 @@ local function setCurrentScope(scope: Effect | EffectScope?)
|
|||
return prevScope
|
||||
end
|
||||
|
||||
local function onEffectCleanup(cleanup: () -> ())
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
local function onScopeDispose(cleanup: () -> ())
|
||||
if activeScope then
|
||||
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 cleanupEffect(e: Effect | EffectScope)
|
||||
for _, cleanup in e.cleanups :: { () -> () } do
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result: unknown = noYield(cleanup)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
table.clear(e.cleanups)
|
||||
end
|
||||
|
||||
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
||||
cleanupEffect(e)
|
||||
local prev = setCurrentSub(e)
|
||||
startTracking(e)
|
||||
local success, result: unknown = noYield((e :: Effect).fn)
|
||||
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(e)
|
||||
endTracking(effect)
|
||||
setCurrentTracking(prevTracking)
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
table.insert(effect.cleanups, result)
|
||||
end
|
||||
return
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
(e :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
elseif bit32.btest(flags, PENDING) then
|
||||
(effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
local link = e.deps
|
||||
local link = effect.deps
|
||||
while link do
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
if bit32.band(depFlags, QUEUED) ~= 0 then
|
||||
if bit32.btest(depFlags, QUEUED) then
|
||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||
dep.flags = depFlags
|
||||
run(dep :: Effect, depFlags)
|
||||
|
|
@ -179,209 +189,154 @@ local function endBatch()
|
|||
end
|
||||
end
|
||||
|
||||
--- @deprecated Will be removed in the next major version. Use
|
||||
--- `const pausedSub = setCurrentSub(undefined)` instead for better performance.
|
||||
local function pauseTracking()
|
||||
table.insert(pauseStack, setCurrentSub(nil))
|
||||
end
|
||||
|
||||
--- @deprecated Will be removed in the next major version. Use
|
||||
--- `setCurrentSub(pausedSub)` instead for better performance.
|
||||
local function resumeTracking()
|
||||
setCurrentSub(table.remove(pauseStack))
|
||||
end
|
||||
|
||||
local function updateComputed(c: Computed): boolean
|
||||
local prevSub = setCurrentSub(c)
|
||||
startTracking(c)
|
||||
local oldValue = c.value
|
||||
local success, result = noYield(c.getter, oldValue)
|
||||
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(c)
|
||||
endTracking(computed)
|
||||
setCurrentTracking(prevTracking)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
if oldValue ~= result then
|
||||
c.value = result
|
||||
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 updateSignal(s: Signal, value: any): boolean
|
||||
s.flags = MUTABLE
|
||||
if s.previousValue ~= value then
|
||||
s.previousValue = value
|
||||
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 signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||
local function getter(): T
|
||||
local value = this.value
|
||||
if bit32.band(this.flags, DIRTY) ~= 0 then
|
||||
if updateSignal(this, value) then
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
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
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
end
|
||||
return value
|
||||
end
|
||||
if activeSub and activeSubTracking then
|
||||
link(atom, activeSub)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function setter(newValue: ((T) -> T) | T): T
|
||||
local value = this.value
|
||||
local newValue = newValue :: T
|
||||
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 = noYield(newValue, value)
|
||||
local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
newValue = result
|
||||
end
|
||||
if value ~= newValue then
|
||||
this.value = newValue
|
||||
this.flags = bit32.bor(MUTABLE, DIRTY)
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
propagate(subs)
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
return newValue
|
||||
return writeAtom(atom, oldValue, newValue :: T)
|
||||
end
|
||||
|
||||
return getter, setter
|
||||
end
|
||||
|
||||
local function computedGetter<T>(this: Computed<T>): () -> T
|
||||
local function computedGetter<T>(computed: Computed<T>): () -> T
|
||||
return function()
|
||||
local flags = this.flags
|
||||
local flags = computed.flags
|
||||
if
|
||||
bit32.band(flags, DIRTY) ~= 0
|
||||
or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this))
|
||||
bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
|
||||
then
|
||||
if updateComputed(this) then
|
||||
local subs = this.subs
|
||||
if updateComputed(computed) then
|
||||
local subs = computed.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
elseif bit32.btest(flags, PENDING) then
|
||||
computed.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
if activeSubTracking then
|
||||
link(computed, activeSub)
|
||||
end
|
||||
elseif activeScope then
|
||||
link(this, activeScope)
|
||||
link(computed, activeScope)
|
||||
end
|
||||
return this.value :: T
|
||||
return computed.value :: T
|
||||
end
|
||||
end
|
||||
|
||||
local function effectStop(this: Effect | EffectScope): () -> ()
|
||||
local function effectStop(self: Effect | EffectScope): () -> ()
|
||||
return function()
|
||||
local dep = this.deps
|
||||
local dep = self.deps
|
||||
while dep do
|
||||
dep = unlink(dep, this)
|
||||
dep = unlink(dep, self)
|
||||
end
|
||||
local sub = this.subs
|
||||
local sub = self.subs
|
||||
if sub then
|
||||
unlink(sub)
|
||||
end
|
||||
(this :: Effect).flags = NONE
|
||||
cleanupEffect(this)
|
||||
(self :: Effect).flags = NONE
|
||||
runCleanups(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function signal<T>(initialValue: T): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||
return signalGetterSetter({
|
||||
previousValue = initialValue,
|
||||
value = initialValue,
|
||||
flags = MUTABLE,
|
||||
})
|
||||
end
|
||||
|
||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||
return computedGetter({
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
getter = getter,
|
||||
})
|
||||
end
|
||||
|
||||
local function effect(fn: () -> ()): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
cleanups = {},
|
||||
flags = WATCHING,
|
||||
}
|
||||
if activeSub then
|
||||
link(e, activeSub)
|
||||
elseif activeScope then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result: unknown = noYield(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
return effectStop(e)
|
||||
end
|
||||
|
||||
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||
local e: EffectScope = {
|
||||
cleanups = {},
|
||||
flags = NONE,
|
||||
}
|
||||
if activeScope and not detached then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local prevScope = setCurrentScope(e)
|
||||
local success, result: unknown = noYield(fn)
|
||||
setCurrentScope(prevScope)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
return effectStop(e)
|
||||
end
|
||||
|
||||
local function update(signal: Signal | Computed): boolean
|
||||
if (signal :: Computed).getter then
|
||||
return updateComputed(signal :: Computed)
|
||||
local function update(atom: Atom | Computed): boolean
|
||||
if (atom :: Computed).getter then
|
||||
return updateComputed(atom :: Computed)
|
||||
else
|
||||
return updateSignal(signal :: Signal, signal.value)
|
||||
return updateAtom(atom :: Atom, atom.value)
|
||||
end
|
||||
end
|
||||
|
||||
local function notify(e: Effect | EffectScope)
|
||||
local flags = e.flags
|
||||
if bit32.band(flags, QUEUED) == 0 then
|
||||
(e :: Effect).flags = bit32.bor(flags, QUEUED)
|
||||
local subs = e.subs
|
||||
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] = e
|
||||
queuedEffects[queuedEffectsLength] = effect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function unwatched(node: Signal | Computed | Effect | EffectScope)
|
||||
local function unwatched(node: Atom | Computed | Effect | EffectScope)
|
||||
if (node :: Computed).getter then
|
||||
local toRemove = node.deps
|
||||
if toRemove then
|
||||
|
|
@ -390,11 +345,73 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope)
|
|||
toRemove = unlink(toRemove, node)
|
||||
until not toRemove
|
||||
end
|
||||
elseif (node :: Effect).cleanups ~= nil then
|
||||
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) -> (),
|
||||
|
|
@ -416,14 +433,12 @@ return {
|
|||
setCurrentSub = setCurrentSub,
|
||||
getCurrentScope = getCurrentScope,
|
||||
setCurrentScope = setCurrentScope,
|
||||
setCurrentTracking = setCurrentTracking,
|
||||
onCleanup = onCleanup,
|
||||
getBatchDepth = getBatchDepth,
|
||||
startBatch = startBatch,
|
||||
endBatch = endBatch,
|
||||
pauseTracking = pauseTracking,
|
||||
resumeTracking = resumeTracking,
|
||||
onEffectCleanup = onEffectCleanup,
|
||||
onScopeDispose = onScopeDispose,
|
||||
signal = signal,
|
||||
atom = atom,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
|
||||
local function effect(callback: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
||||
local disposed = false
|
||||
|
||||
local function dispose()
|
||||
disposed = true
|
||||
end
|
||||
|
||||
dispose = signals.effect(function()
|
||||
local cleanup = callback(dispose)
|
||||
if cleanup then
|
||||
onEffectCleanup(cleanup)
|
||||
end
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return dispose
|
||||
end
|
||||
|
||||
return effect
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
|
||||
local function effectScope(callback: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||
return signals.effectScope(function()
|
||||
local cleanup = callback()
|
||||
if cleanup then
|
||||
onScopeDispose(cleanup)
|
||||
end
|
||||
end, detached)
|
||||
end
|
||||
|
||||
return effectScope
|
||||
1
packages/charm/src/index.d.ts
vendored
Normal file
1
packages/charm/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
local atom = require("@self/atom")
|
||||
local batched = require("@self/batched")
|
||||
local effect = require("@self/effect")
|
||||
local core = require("@self/core")
|
||||
local listen = require("@self/listen")
|
||||
local mapped = require("@self/mapped")
|
||||
local observe = require("@self/observe")
|
||||
local onCleanup = require("@self/onCleanup")
|
||||
local signals = require("@self/signals")
|
||||
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 Getter<T = any> = types.Getter<T>
|
||||
export type Selector<T = any> = types.Selector<T>
|
||||
export type Setter<T = any> = types.Setter<T>
|
||||
export type Update<T = any> = types.Update<T>
|
||||
|
||||
local computed = signals.computed
|
||||
local effectScope = signals.effectScope
|
||||
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,
|
||||
|
|
@ -28,8 +28,7 @@ return {
|
|||
listen = listen,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
onCleanup = onCleanup,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
onCleanup = onCleanup,
|
||||
signals = signals,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
|
|
@ -20,12 +19,7 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
|
|||
dispose = effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
|
||||
local stopScope = effectScope(function()
|
||||
listener(current, previous, dispose)
|
||||
end)
|
||||
|
||||
onEffectCleanup(stopScope)
|
||||
untracked(listener, current, previous, dispose)
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
|
|
|
|||
|
|
@ -1,35 +1,26 @@
|
|||
local noYield = require("./noYield")
|
||||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
|
||||
local computed = signals.computed
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
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 ensureCanMutateOutput<KO, VO>(output: { [KO]: VO }, previous: { [KO]: VO }?): { [KO]: VO }
|
||||
return if output == previous then table.clone(output) else output
|
||||
end
|
||||
|
||||
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 = noYield(mapper, value, key)
|
||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`mapped() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
if keyOut == nil then
|
||||
keyOut = key :: any
|
||||
end
|
||||
|
||||
return valueOut, keyOut
|
||||
return valueOut, if keyOut == nil then key else keyOut :: any
|
||||
end
|
||||
|
||||
return computed(function(previousOutput)
|
||||
|
|
@ -41,11 +32,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
|||
if currentData[key] ~= nil then
|
||||
continue
|
||||
end
|
||||
|
||||
local cachedKey = keyCache[key]
|
||||
|
||||
if cachedKey then
|
||||
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
||||
currentOutput[cachedKey] = nil
|
||||
keyCache[key] = nil
|
||||
end
|
||||
|
|
@ -55,11 +44,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
|||
if value == previousData[key] then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = map(value, key)
|
||||
|
||||
if currentOutput[keyOut] ~= valueOut then
|
||||
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
||||
currentOutput[keyOut] = valueOut
|
||||
keyCache[key] = keyOut
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
local onCleanup = require("./onCleanup")
|
||||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
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]: () -> () } = {}
|
||||
|
|
@ -11,8 +10,9 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
|||
|
||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||
for key in previousItems do
|
||||
if currentItems[key] == nil and scopes[key] then
|
||||
scopes[key]()
|
||||
local dispose = scopes[key]
|
||||
if currentItems[key] == nil and dispose then
|
||||
dispose()
|
||||
scopes[key] = nil
|
||||
end
|
||||
end
|
||||
|
|
@ -20,10 +20,7 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
|||
for key, value in currentItems do
|
||||
if previousItems[key] == nil then
|
||||
scopes[key] = effectScope(function()
|
||||
local cleanup = observer(value, key)
|
||||
if cleanup then
|
||||
onScopeDispose(cleanup)
|
||||
end
|
||||
return observer(value, key)
|
||||
end, true)
|
||||
end
|
||||
end
|
||||
|
|
@ -39,7 +36,6 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
|||
local disposeEffect = effect(function()
|
||||
local previousItems = currentItems
|
||||
currentItems = selector()
|
||||
|
||||
if currentItems ~= previousItems then
|
||||
updateScopes(currentItems, previousItems)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
local getCurrentScope = signals.getCurrentScope
|
||||
local getCurrentSub = signals.getCurrentSub
|
||||
|
||||
local warn = warn or error :: never
|
||||
|
||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
||||
if getCurrentSub() then
|
||||
onEffectCleanup(cleanup)
|
||||
elseif getCurrentScope() then
|
||||
onScopeDispose(cleanup)
|
||||
elseif _G.__DEV__ and not failSilently then
|
||||
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
||||
end
|
||||
end
|
||||
|
||||
return onCleanup
|
||||
8
packages/charm/src/strict.luau
Normal file
8
packages/charm/src/strict.luau
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
local function inlineTest(): string
|
||||
return debug.info(1, "n")
|
||||
end
|
||||
|
||||
return {
|
||||
-- Enable strict mode in O2
|
||||
enabled = inlineTest() == "inlineTest",
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
|
|
@ -16,17 +15,11 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
|
|||
return effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
|
||||
if mounted then
|
||||
mounted = false
|
||||
return
|
||||
else
|
||||
untracked(listener, current, previous)
|
||||
end
|
||||
|
||||
local stopScope = effectScope(function()
|
||||
listener(current, previous)
|
||||
end)
|
||||
|
||||
onEffectCleanup(stopScope)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ local function createReactiveSystem(props: {
|
|||
return
|
||||
end
|
||||
local nextDep: Link?
|
||||
local recursedCheck = bit32.band(sub.flags, RECURSED_CHECK) ~= 0
|
||||
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
|
||||
|
|
@ -178,14 +178,14 @@ local function createReactiveSystem(props: {
|
|||
|
||||
local flags = sub.flags
|
||||
|
||||
if bit32.band(flags, bit32.bor(MUTABLE, WATCHING)) ~= 0 then
|
||||
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||
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 bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
|
||||
flags = NONE
|
||||
elseif bit32.band(flags, RECURSED_CHECK) == 0 then
|
||||
elseif not bit32.btest(flags, RECURSED_CHECK) then
|
||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||
elseif bit32.band(flags, bit32.bor(DIRTY, PENDING)) == 0 and isValidLink(link, sub) then
|
||||
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
|
||||
|
|
@ -193,11 +193,11 @@ local function createReactiveSystem(props: {
|
|||
flags = NONE
|
||||
end
|
||||
|
||||
if bit32.band(flags, WATCHING) ~= 0 then
|
||||
if bit32.btest(flags, WATCHING) then
|
||||
notify(sub)
|
||||
end
|
||||
|
||||
if bit32.band(flags, MUTABLE) ~= 0 then
|
||||
if bit32.btest(flags, MUTABLE) then
|
||||
local subSubs = sub.subs
|
||||
if subSubs then
|
||||
link = subSubs
|
||||
|
|
@ -257,7 +257,7 @@ local function createReactiveSystem(props: {
|
|||
local subFlags = sub.flags
|
||||
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then
|
||||
sub.flags = bit32.bor(subFlags, DIRTY)
|
||||
if bit32.band(subFlags, WATCHING) ~= 0 then
|
||||
if bit32.btest(subFlags, WATCHING) then
|
||||
notify(sub)
|
||||
end
|
||||
end
|
||||
|
|
@ -275,7 +275,7 @@ local function createReactiveSystem(props: {
|
|||
|
||||
local dirty = false
|
||||
|
||||
if bit32.band(sub.flags, DIRTY) ~= 0 then
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
export type Getter<T = any> = () -> T
|
||||
|
||||
export type Setter<T = any> = (update: Update<T>) -> T
|
||||
|
||||
export type Update<T = any> = T | (previous: T) -> T
|
||||
|
||||
export type Selector<T = any> = () -> T
|
||||
|
||||
export type Atom<T = any> = (...Update<T>) -> T
|
||||
type Update<T> = ((previous: T) -> T) | T
|
||||
export type Atom<T> = (...Update<T>) -> T
|
||||
export type Selector<T> = () -> T
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
local noYield = require("./noYield")
|
||||
local signals = require("./signals")
|
||||
local core = require("./core")
|
||||
|
||||
type ReactiveNode = signals.ReactiveNode
|
||||
local setCurrentTracking = core.setCurrentTracking
|
||||
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function handleUntracked(currentSub: ReactiveNode?, ok: boolean, ...)
|
||||
setCurrentSub(currentSub)
|
||||
local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
||||
setCurrentTracking(prevTracking)
|
||||
if ok then
|
||||
return ...
|
||||
else
|
||||
error(...)
|
||||
error((...))
|
||||
end
|
||||
end
|
||||
|
||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
return handleUntracked(setCurrentSub(nil), noYield(callback, ...))
|
||||
return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
|
||||
end
|
||||
|
||||
return untracked
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
local function handleNoYield(thread: thread, ok: boolean, ...)
|
||||
if not ok then
|
||||
return false, debug.traceback(thread, tostring((...)))
|
||||
end
|
||||
|
||||
if coroutine.status(thread) ~= "dead" then
|
||||
elseif coroutine.status(thread) ~= "dead" then
|
||||
return false, debug.traceback(thread, "Attempted to yield!")
|
||||
else
|
||||
return true, ...
|
||||
end
|
||||
|
||||
return true, ...
|
||||
end
|
||||
|
||||
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
||||
|
|
@ -15,6 +13,6 @@ local function noYield<Args..., Results...>(callback: (Args...) -> Results..., .
|
|||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
||||
end
|
||||
|
||||
-- To eliminate the overhead of noYield returning callback(...) in production,
|
||||
-- replace it with pcall to simulate the same behavior.
|
||||
return if _G.__DEV__ then noYield else pcall :: never
|
||||
return {
|
||||
noYield = noYield :: typeof(pcall),
|
||||
}
|
||||
6
packages/charm/test/default.project.json
Normal file
6
packages/charm/test/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
20
packages/charm/test/src/batched.luau
Normal file
20
packages/charm/test/src/batched.luau
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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
|
||||
445
packages/charm/test/src/core.luau
Normal file
445
packages/charm/test/src/core.luau
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
--[[
|
||||
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,
|
||||
}
|
||||
34
packages/charm/test/src/init.luau
Normal file
34
packages/charm/test/src/init.luau
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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,
|
||||
}
|
||||
32
packages/charm/test/src/listen.luau
Normal file
32
packages/charm/test/src/listen.luau
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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
|
||||
59
packages/charm/test/src/mapped.luau
Normal file
59
packages/charm/test/src/mapped.luau
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
52
packages/charm/test/src/observe.luau
Normal file
52
packages/charm/test/src/observe.luau
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
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
|
||||
8
packages/charm/test/src/strict.luau
Normal file
8
packages/charm/test/src/strict.luau
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
local function inlineTest(): string
|
||||
return debug.info(1, "n")
|
||||
end
|
||||
|
||||
return {
|
||||
-- Enable strict mode in O2
|
||||
enabled = inlineTest() == "inlineTest",
|
||||
}
|
||||
26
packages/charm/test/src/subscribe.luau
Normal file
26
packages/charm/test/src/subscribe.luau
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
357
packages/charm/test/src/system.luau
Normal file
357
packages/charm/test/src/system.luau
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
--[[
|
||||
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,
|
||||
}
|
||||
5
packages/charm/test/src/types.luau
Normal file
5
packages/charm/test/src/types.luau
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
type Update<T> = ((previous: T) -> T) | T
|
||||
export type Atom<T> = (...Update<T>) -> T
|
||||
export type Selector<T> = () -> T
|
||||
|
||||
return {}
|
||||
18
packages/charm/test/src/untracked.luau
Normal file
18
packages/charm/test/src/untracked.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
18
packages/charm/test/src/utils.luau
Normal file
18
packages/charm/test/src/utils.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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),
|
||||
}
|
||||
8
packages/charm/test/wally.toml
Normal file
8
packages/charm/test/wally.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[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]
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
local suite = require("@test/suite")
|
||||
|
||||
suite.describe("signals", function()
|
||||
suite.describe("computed", function()
|
||||
require("./computed.spec")
|
||||
end)
|
||||
suite.describe("effect", function()
|
||||
require("./effect.spec")
|
||||
end)
|
||||
suite.describe("effectScope", function()
|
||||
require("./effectScope.spec")
|
||||
end)
|
||||
suite.describe("issue_48", function()
|
||||
require("./issue_48.spec")
|
||||
end)
|
||||
suite.describe("gc", function()
|
||||
require("./gc.spec")
|
||||
end)
|
||||
suite.describe("topology", function()
|
||||
require("./topology.spec")
|
||||
end)
|
||||
suite.describe("untrack", function()
|
||||
require("./untrack.spec")
|
||||
end)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
local suite = require("@test/suite")
|
||||
|
||||
require("./signals/spec")
|
||||
|
||||
suite.describe("charm", function()
|
||||
suite.describe("listen", function()
|
||||
require("./listen.spec")
|
||||
end)
|
||||
suite.describe("mapped", function()
|
||||
require("./mapped.spec")
|
||||
end)
|
||||
suite.describe("observe", function()
|
||||
require("./observe.spec")
|
||||
end)
|
||||
suite.describe("subscribe", function()
|
||||
require("./subscribe.spec")
|
||||
end)
|
||||
suite.describe("untracked", function()
|
||||
require("./untracked.spec")
|
||||
end)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -3,6 +3,6 @@ name = "littensy/charm"
|
|||
version = "0.10.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["tests"]
|
||||
exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"]
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
6
packages/react-charm/Charm.luau
Normal file
6
packages/react-charm/Charm.luau
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
local Charm = require(script.Parent.Parent.charm.src)
|
||||
|
||||
export type Atom<T = any> = Charm.Atom<T>
|
||||
export type Selector<T = any> = Charm.Selector<T>
|
||||
|
||||
return Charm
|
||||
12
packages/react-charm/React.luau
Normal file
12
packages/react-charm/React.luau
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
type CoreReactBinding<T> = {
|
||||
getValue: (self: CoreReactBinding<T>) -> T,
|
||||
_source: string?,
|
||||
}
|
||||
|
||||
type ReactBindingMap = {
|
||||
map: <T, U>(self: CoreReactBinding<T> & ReactBindingMap, (T) -> U) -> ReactBindingMap & CoreReactBinding<U>,
|
||||
}
|
||||
|
||||
export type Binding<T> = CoreReactBinding<T> & ReactBindingMap
|
||||
|
||||
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").React)
|
||||
1
packages/react-charm/ReactRoblox.luau
Normal file
1
packages/react-charm/ReactRoblox.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").ReactRoblox)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "ReactCharm",
|
||||
"name": "react-charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
|
|
|
|||
30
packages/react-charm/package.json
Normal file
30
packages/react-charm/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@rbxts/react-charm",
|
||||
"version": "0.3.0",
|
||||
"description": "React bindings for Charm state management",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"*.luau",
|
||||
"!wally.toml",
|
||||
"!wally.lock",
|
||||
"!default.project.json",
|
||||
"!**/__tests__"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/littensy/charm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/react": "*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||
|
||||
local React = require(ReplicatedStorage.Packages.React)
|
||||
local ReactRoblox = require(ReplicatedStorage.Packages.ReactRoblox)
|
||||
local React = require("../../../React")
|
||||
local ReactRoblox = require("../../../ReactRoblox")
|
||||
|
||||
type RenderedHook<Props, Result> = {
|
||||
result: Result,
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("react-charm", function()
|
||||
if not game then
|
||||
return suite.skip()
|
||||
end
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
end)
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
|
||||
local renderHook = require(script.Parent.helpers.renderHook)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local ReactCharm = require("../")
|
||||
local renderHook = require("./helpers/renderHook")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local useAtom = ReactCharm.useAtom
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
|
||||
local renderHook = require(script.Parent.helpers.renderHook)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local ReactCharm = require("../")
|
||||
local renderHook = require("./helpers/renderHook")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local useAtomBinding = ReactCharm.useAtomBinding
|
||||
1
packages/react-charm/src/index.d.ts
vendored
Normal file
1
packages/react-charm/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local React = require(script.Parent.React)
|
||||
local Charm = require("./Charm")
|
||||
local React = require("./React")
|
||||
|
||||
local effect = Charm.effect
|
||||
local useBinding = React.useBinding
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ name = "littensy/react-charm"
|
|||
version = "0.3.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["tests"]
|
||||
exclude = [
|
||||
"node_modules",
|
||||
"src/__tests__",
|
||||
"src/index.d.ts",
|
||||
"package.json",
|
||||
"React.luau",
|
||||
"Charm.luau",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
|
|
|
|||
6
packages/vide-charm/Charm.luau
Normal file
6
packages/vide-charm/Charm.luau
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
local Charm = require(script.Parent.Parent.charm.src)
|
||||
|
||||
export type Atom<T = any> = Charm.Atom<T>
|
||||
export type Selector<T = any> = Charm.Selector<T>
|
||||
|
||||
return Charm
|
||||
1
packages/vide-charm/Vide.luau
Normal file
1
packages/vide-charm/Vide.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require(script.Parent.Parent.vide.src)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "VideCharm",
|
||||
"name": "vide-charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
|
|
|
|||
30
packages/vide-charm/package.json
Normal file
30
packages/vide-charm/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@rbxts/vide-charm",
|
||||
"version": "0.3.0",
|
||||
"description": "Vide bindings for Charm state management",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"*.luau",
|
||||
"!wally.toml",
|
||||
"!wally.lock",
|
||||
"!default.project.json",
|
||||
"!**/__tests__"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/littensy/charm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE.md . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/vide": "*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("vide-charm", function()
|
||||
if not game then
|
||||
return suite.skip()
|
||||
end
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
end)
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local Vide = require(ReplicatedStorage.Packages.Vide)
|
||||
local VideCharm = require(ReplicatedStorage.Packages.VideCharm)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
local Charm = require("../../Charm")
|
||||
local Vide = require("../../Vide")
|
||||
local VideCharm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local root = Vide.root
|
||||
1
packages/vide-charm/src/index.d.ts
vendored
Normal file
1
packages/vide-charm/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local Vide = require(script.Parent.Vide)
|
||||
local Charm = require("./Charm")
|
||||
local Vide = require("./Vide")
|
||||
|
||||
local subscribe = Charm.subscribe
|
||||
local source = Vide.source
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ name = "littensy/vide-charm"
|
|||
version = "0.3.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["tests"]
|
||||
exclude = [
|
||||
"node_modules",
|
||||
"src/__tests__",
|
||||
"src/index.d.ts",
|
||||
"package.json",
|
||||
"Vide.luau",
|
||||
"Charm.luau",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.10"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue