refactor: improve structure and alien-signals integration

This commit is contained in:
littensy 2025-07-02 09:25:50 -07:00
parent a9c60c701f
commit 95476e93ec
107 changed files with 3660 additions and 815 deletions

28
.eslintrc Normal file
View file

@ -0,0 +1,28 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"jsx": true,
"useJSXTextNode": true,
"ecmaVersion": 2018,
"sourceType": "module",
"project": [
"./tsconfig.json",
"./examples/ts/tsconfig.json"
]
},
"plugins": [
"@typescript-eslint",
"roblox-ts",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:roblox-ts/recommended",
"plugin:prettier/recommended"
],
"rules": {
"prettier/prettier": "warn",
"@typescript-eslint/no-explicit-any": "off"
}
}

2
.gitignore vendored
View file

@ -2,6 +2,8 @@
/.pesde /.pesde
/build /build
/packages/**/wally.lock /packages/**/wally.lock
/packages/**/LICENSE.md
/packages/**/README.md
/sourcemap.json /sourcemap.json
node_modules node_modules
*.tsbuildinfo *.tsbuildinfo

View file

@ -2,9 +2,5 @@
"languageMode": "strict", "languageMode": "strict",
"lint": { "lint": {
"LocalShadow": false "LocalShadow": false
},
"aliases": {
"test": "./test",
"packages": "./packages"
} }
} }

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": true
}

View file

@ -1,10 +1,10 @@
local Charm = require("@packages/charm/src") local Charm = require("../packages/charm/src")
local bench = require("./bench") local bench = require("./bench")
local atom = Charm.atom local atom = Charm.atom
local computed = Charm.computed local computed = Charm.computed
local mapped = Charm.mapped local mapped = Charm.mapped
local setCurrentSub = Charm.signals.setCurrentSub local setCurrentSub = Charm.core.setCurrentSub
type Entry = { number } type Entry = { number }

View file

@ -1,6 +1,4 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local CharmSync = require("../packages/charm-sync/src")
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local patch = CharmSync.patch local patch = CharmSync.patch

View file

@ -1,42 +1,27 @@
local Charm = require("@packages/charm/src") local Charm = require("../packages/charm/src")
local bench = require("./bench") local bench = require("./bench")
local signals = Charm.signals local atom = Charm.atom
local computed = Charm.computed
local effect = Charm.effect
for _, w in { 10, 100 } do for _, w in { 10, 100 } do
for _, h in { 10, 100 } do for _, h in { 10, 100 } do
bench.run(`charm {w}*{h}`, function() bench.run(`propagate {w}*{h}`, function()
local src = Charm.atom(1) local src = atom(1)
for _ = 1, w do for _ = 1, w do
local last = src local last = src
for _ = 1, h do for _ = 1, h do
local prev = last local prev = last
last = Charm.computed(function() last = computed(function()
return prev() + 1 return prev() + 1
end) end)
end end
Charm.effect(function() effect(function()
last() last()
end) end)
end end
src(src() + 1) src(src() + 1)
end) end)
bench.run(`alien-signals {w}*{h}`, function()
local src, setSrc = signals.signal(1)
for _ = 1, w do
local last = src
for _ = 1, h do
local prev = last
last = signals.computed(function()
return prev() + 1
end)
end
signals.effect(function()
last()
end)
end
setSrc(src() + 1)
end)
end end
end end

View file

@ -1,5 +1,4 @@
#!/usr/bin/env bash
mkdir build mkdir build
rojo build packages/charm -o build/charm.rbxm rojo build build.project.json -o build/charm.rbxm
rojo build packages/charm-sync -o build/charm-sync.rbxm
rojo build packages/react-charm -o build/react-charm.rbxm
rojo build packages/vide-charm -o build/vide-charm.rbxm

View file

@ -5,12 +5,15 @@ rojo sourcemap -o sourcemap.json
luau-lsp analyze \ luau-lsp analyze \
--defs=bin/roblox.d.luau \ --defs=bin/roblox.d.luau \
--flag:LuauFixIndexerSubtypingOrdering=true \ --flag:LuauFixIndexerSubtypingOrdering=true \
--flag:LuauInstantiateInSubtyping=true \
--sourcemap=sourcemap.json \ --sourcemap=sourcemap.json \
--ignore="**/node_modules/**" \ --ignore="**/node_modules/**" \
packages test packages test benches
selene packages test selene packages test benches
stylua --check packages test stylua --check packages test benches
pnpm eslint packages
rm bin/roblox.d.luau rm bin/roblox.d.luau

10
build.project.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "Packages",
"tree": {
"$className": "Folder",
"Charm": { "$path": "packages/charm/src" },
"CharmSync": { "$path": "packages/charm-sync/src" },
"ReactCharm": { "$path": "packages/react-charm/src" },
"VideCharm": { "$path": "packages/vide-charm/src" }
}
}

View file

@ -1,29 +1,50 @@
{ {
"name": "charm-test", "name": "charm-test",
"globIgnorePaths": [
"**/default.project.json",
"node_modules/charm",
"node_modules/charm-sync",
"node_modules/react-charm",
"node_modules/vide-charm"
],
"emitLegacyScripts": false,
"tree": { "tree": {
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"$className": "ReplicatedStorage", "$className": "ReplicatedStorage",
"Packages": { "packages": {
"$path": "node_modules/@rbxts-js", "$className": "Folder",
"Vide": { "$path": "node_modules/@rbxts/vide/src" }, "vide": {
"Charm": { "$path": "packages/charm" }, "$path": "node_modules/@rbxts/vide"
"CharmSync": { "$path": "packages/charm-sync" }, },
"ReactCharm": { "$path": "packages/react-charm" }, "charm": {
"VideCharm": { "$path": "packages/vide-charm" } "$className": "Folder",
"src": { "$path": "packages/charm/src" }
},
"charm-sync": {
"$className": "Folder",
"src": { "$path": "packages/charm-sync/src" },
"Charm": { "$path": "packages/charm-sync/Charm.luau" }
},
"react-charm": {
"$className": "Folder",
"src": { "$path": "packages/react-charm/src" },
"React": { "$path": "packages/react-charm/React.luau" },
"ReactRoblox": { "$path": "packages/react-charm/ReactRoblox.luau" },
"Charm": { "$path": "packages/react-charm/Charm.luau" }
},
"vide-charm": {
"$className": "Folder",
"src": { "$path": "packages/vide-charm/src" },
"Vide": { "$path": "packages/vide-charm/Vide.luau" },
"Charm": { "$path": "packages/vide-charm/Charm.luau" }
} }
}, },
"ServerScriptService": { "@rbxts-js": {
"$className": "ServerScriptService", "$path": "node_modules/@rbxts-js"
"Tests": {
"$path": "test",
"Charm": { "$path": "packages/charm/tests" },
"CharmSync": { "$path": "packages/charm-sync/tests" },
"ReactCharm": { "$path": "packages/react-charm/tests" },
"VideCharm": { "$path": "packages/vide-charm/tests" }
}, },
"Benchmarks": { "test": {
"$path": "benches" "$path": "test"
} }
} }
} }

View file

@ -1,14 +1,29 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"test": "lune run test", "test": "lune run test/spec.server.luau",
"build": "sh bin/build.sh", "build": "sh bin/build.sh",
"typecheck": "sh bin/check.sh" "check": "sh bin/check.sh",
"publish:npm": "pnpm publish -r --access public",
"publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally"
}, },
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.12.4",
"devDependencies": { "devDependencies": {
"@rbxts-js/react": "17.1.20-ts.1", "@rbxts-js/react": "17.1.20-ts.1",
"@rbxts-js/react-roblox": "17.1.20-ts.1", "@rbxts-js/react-roblox": "17.1.20-ts.1",
"@rbxts/vide": "^0.5.7" "@rbxts/compiler-types": "3.0.0-types.0",
"@rbxts/react": "17.3.0-alpha.1",
"@rbxts/react-roblox": "17.3.0-alpha.1",
"@rbxts/types": "^1.0.861",
"@rbxts/vide": "^0.5.7",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-roblox-ts": "^0.0.36",
"prettier": "^3.6.2",
"roblox-ts": "^3.0.0",
"typescript": "^5.8.3"
} }
} }

View 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

View file

@ -1,5 +1,5 @@
{ {
"name": "CharmSync", "name": "charm-sync",
"tree": { "tree": {
"$path": "src" "$path": "src"
} }

View 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:*"
}
}

View file

@ -1,9 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local CharmSync = require("../")
local suite = require("../../../../test/suite")
local Charm = require(ReplicatedStorage.Packages.Charm)
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local subscribe = Charm.subscribe local subscribe = Charm.subscribe

View file

@ -1,9 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local CharmSync = require("../")
local suite = require("../../../../test/suite")
local Charm = require(ReplicatedStorage.Packages.Charm)
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local flatten = CharmSync.flatten local flatten = CharmSync.flatten

View file

@ -1,9 +1,7 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local CharmSync = require(ReplicatedStorage.Packages.CharmSync) local CharmSync = require("../")
local suite = require(ServerScriptService.Tests.suite) local suite = require("../../../../test/suite")
local patch = CharmSync.patch local patch = CharmSync.patch
local test = suite.test local test = suite.test

View file

@ -1,9 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local CharmSync = require("../")
local suite = require("../../../../test/suite")
local Charm = require(ReplicatedStorage.Packages.Charm)
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local suite = require(ServerScriptService.Tests.suite)
type SyncPayload = CharmSync.SyncPayload type SyncPayload = CharmSync.SyncPayload

View file

@ -1,8 +1,9 @@
local ServerScriptService = game:GetService("ServerScriptService") local suite = require("../../../../test/suite")
local suite = require(ServerScriptService.Tests.suite)
suite.describe("charm-sync", function() suite.describe("charm-sync", function()
if not game then
return suite.skip()
end
suite.describe("client", function() suite.describe("client", function()
require("./client.spec") require("./client.spec")
end) end)

View file

@ -1,9 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local CharmSync = require("../")
local suite = require("../../../../test/suite")
local Charm = require(ReplicatedStorage.Packages.Charm)
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local validate = CharmSync.validate local validate = CharmSync.validate

View file

@ -1,6 +1,6 @@
local Charm = require(script.Parent.Parent.Charm) local Charm = require("../Charm")
local patch = require(script.Parent.patch) local patch = require("./patch")
local types = require(script.Parent.types) local types = require("./types")
type AtomMap = types.AtomMap type AtomMap = types.AtomMap
type SyncPayload = types.SyncPayload type SyncPayload = types.SyncPayload
@ -36,7 +36,7 @@ local function client(options: ClientOptions): ClientSyncer
local function hydrate(state: { [string | number]: any }) local function hydrate(state: { [string | number]: any })
hydrated = true hydrated = true
for key, value in next, state do for key, value in state do
local atom = atoms[key] local atom = atoms[key]
atom(value) atom(value)
end end
@ -45,13 +45,13 @@ local function client(options: ClientOptions): ClientSyncer
local function apply(data: { [string | number]: any }) local function apply(data: { [string | number]: any })
local target = {} local target = {}
for key, atom in next, atoms do for key, atom in atoms do
target[key] = atom() target[key] = atom()
end end
target = patch.apply(target, data) target = patch.apply(target, data)
for key, atom in next, atoms do for key, atom in atoms do
atom(target[key]) atom(target[key])
end end
end end

View file

@ -1,4 +1,4 @@
local types = require(script.Parent.types) local types = require("./types")
type AtomMap = types.AtomMap type AtomMap = types.AtomMap

1
packages/charm-sync/src/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

View file

@ -1,9 +1,9 @@
local client = require(script.client) local client = require("@self/client")
local flatten = require(script.flatten) local flatten = require("@self/flatten")
local patch = require(script.patch) local patch = require("@self/patch")
local server = require(script.server) local server = require("@self/server")
local types = require(script.types) local types = require("@self/types")
local validate = require(script.validate) local validate = require("@self/validate")
export type SyncPayload = types.SyncPayload export type SyncPayload = types.SyncPayload

View file

@ -1,5 +1,3 @@
local RunService = game:GetService("RunService")
--- Schedules a function to be called at a fixed interval of `seconds`. --- Schedules a function to be called at a fixed interval of `seconds`.
--- ---
--- @param callback The function to call. --- @param callback The function to call.
@ -12,7 +10,7 @@ local function interval(callback: () -> (), seconds: number): () -> ()
local clock = 0 local clock = 0
local connection = RunService.Heartbeat:Connect(function(delta) local connection = game:GetService("RunService").Heartbeat:Connect(function(delta)
clock += delta clock += delta
if clock >= seconds then if clock >= seconds then
clock = 0 clock = 0

View file

@ -1,6 +1,5 @@
local validate = require(script.Parent.validate) local Charm = require("../Charm")
local validate = require("./validate")
local __DEV__ = _G.__DEV__
--- A special symbol that denotes the absence of a value. Used to represent --- A special symbol that denotes the absence of a value. Used to represent
--- deleted values in patches. --- deleted values in patches.
@ -64,7 +63,7 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serial
patches = stringifySparseArray(patches) patches = stringifySparseArray(patches)
end end
if serialize and __DEV__ then if serialize and Charm.strict.enabled then
for key, value in next, patches do for key, value in next, patches do
validate(value, key) validate(value, key)
end end

View file

@ -1,9 +1,7 @@
local Players = game:GetService("Players") local Charm = require("../Charm")
local interval = require("./interval")
local Charm = require(script.Parent.Parent.Charm) local patch = require("./patch")
local interval = require(script.Parent.interval) local types = require("./types")
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type AtomMap = types.AtomMap type AtomMap = types.AtomMap
type SyncPayload = types.SyncPayload type SyncPayload = types.SyncPayload
@ -64,7 +62,7 @@ local function server(options: ServerOptions): ServerSyncer
local function createSnapshot() local function createSnapshot()
local snapshot = {} local snapshot = {}
for key, atom in next, atoms do for key, atom in atoms do
snapshot[key] = atom() snapshot[key] = atom()
end end
@ -110,7 +108,7 @@ local function server(options: ServerOptions): ServerSyncer
-- Populate the snapshot with the initial state of each atom. -- Populate the snapshot with the initial state of each atom.
-- Subscribe to each atom and update the state when it changes. -- 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) subscriptions[key] = Charm.subscribe(atom, function(current, previous)
pushSnapshot(key, current, previous) pushSnapshot(key, current, previous)
changed = true changed = true
@ -125,7 +123,7 @@ local function server(options: ServerOptions): ServerSyncer
local payloads: { SyncPayload } = {} local payloads: { SyncPayload } = {}
local lastSnapshot local lastSnapshot
for index, snapshot in next, snapshots do for index, snapshot in snapshots do
lastSnapshot = snapshot lastSnapshot = snapshot
if index == 1 then if index == 1 then
@ -141,7 +139,7 @@ local function server(options: ServerOptions): ServerSyncer
snapshots = { lastSnapshot } snapshots = { lastSnapshot }
changed = false changed = false
for _, player in next, Players:GetPlayers() do for _, player in game:GetService("Players"):GetPlayers() do
callback(player, unpack(payloads)) callback(player, unpack(payloads))
end end
end, syncRate) end, syncRate)
@ -149,7 +147,7 @@ local function server(options: ServerOptions): ServerSyncer
return function() return function()
disconnect() disconnect()
for _, unsubscribe in next, subscriptions do for _, unsubscribe in subscriptions do
unsubscribe() unsubscribe()
end end
end end
@ -164,7 +162,7 @@ local function server(options: ServerOptions): ServerSyncer
local payloads: { SyncPayload } = {} local payloads: { SyncPayload } = {}
for index, snapshot in next, snapshots do for index, snapshot in snapshots do
if index == 1 then if index == 1 then
continue continue
end end
@ -194,7 +192,7 @@ local function server(options: ServerOptions): ServerSyncer
sync = callback sync = callback
-- Subscribe to each atom and update the state when one changes. -- 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) subscriptions[key] = Charm.subscribe(atom, function(state)
currentSnapshot[key] = state currentSnapshot[key] = state
changed = true changed = true
@ -214,7 +212,7 @@ local function server(options: ServerOptions): ServerSyncer
previousSnapshot = table.clone(currentSnapshot) previousSnapshot = table.clone(currentSnapshot)
changed = false changed = false
for _, player in next, Players:GetPlayers() do for _, player in game:GetService("Players"):GetPlayers() do
callback(player, payload) callback(player, payload)
end end
end, syncRate) end, syncRate)
@ -222,7 +220,7 @@ local function server(options: ServerOptions): ServerSyncer
return function() return function()
disconnect() disconnect()
for _, unsubscribe in next, subscriptions do for _, unsubscribe in subscriptions do
unsubscribe() unsubscribe()
end end
end end

View file

@ -1,4 +1,4 @@
local Charm = require(script.Parent.Parent.Charm) local Charm = require("../Charm")
type Atom<T> = Charm.Atom<T> type Atom<T> = Charm.Atom<T>

View file

@ -3,7 +3,13 @@ name = "littensy/charm-sync"
version = "0.3.0" version = "0.3.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
exclude = ["tests"] exclude = [
"node_modules",
"src/__tests__",
"src/index.d.ts",
"package.json",
"Charm.luau",
]
[dependencies] [dependencies]
Charm = "littensy/charm@^0.10" Charm = "littensy/charm@^0.10"

View file

@ -1,5 +1,5 @@
{ {
"name": "Charm", "name": "charm",
"tree": { "tree": {
"$path": "src" "$path": "src"
} }

View 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"
}
}

View file

@ -1,12 +1,12 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local computed = signals.computed local computed = core.computed
local signal = signals.signal local atom = core.atom
local test = suite.test local test = suite.test
test("should correctly propagate changes through computed signals", function() test("should correctly propagate changes through computed core", function()
local src, setSrc = signal(0) local src = atom(0)
local c1 = computed(function() local c1 = computed(function()
return src() % 2 return src() % 2
end) end)
@ -18,15 +18,15 @@ test("should correctly propagate changes through computed signals", function()
end) end)
c3() c3()
setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
c2() -- c1 -> none, c2 -> none c2() -- c1 -> none, c2 -> none
setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty src(3) -- c1 -> dirty, c2 -> toCheckDirty
assert(c3() == 1, "bad final value") assert(c3() == 1, "bad final value")
end) end)
test("should propagate updated source value through chained computations", function() test("should propagate updated source value through chained computations", function()
local src, setSrc = signal(0) local src = atom(0)
local a = computed(function() local a = computed(function()
return src() return src()
end) end)
@ -41,12 +41,12 @@ test("should propagate updated source value through chained computations", funct
end) end)
assert(d() == 0, "bad initial value") assert(d() == 0, "bad initial value")
setSrc(2) src(2)
assert(d() == 2, "bad updated value") assert(d() == 2, "bad updated value")
end) end)
test("should handle flags are indirectly updated during checkDirty", function() test("should handle flags are indirectly updated during checkDirty", function()
local a, setA = signal(false) local a = atom(false)
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -60,22 +60,22 @@ test("should handle flags are indirectly updated during checkDirty", function()
end) end)
assert(d() == false, "bad initial value") assert(d() == false, "bad initial value")
setA(true) a(true)
assert(d() == true, "bad updated value") assert(d() == true, "bad updated value")
end) 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 times = 0
local src, setSrc = signal(0) local src = atom(0)
local c1 = computed(function() local c1 = computed(function()
times += 1 times += 1
return src() return src()
end) end)
c1() c1()
assert(times == 1, "computed did not run") assert(times == 1, "computed did not run")
setSrc(1) src(1)
setSrc(0) src(0)
c1() c1()
assert(times == 1, "computed ran when it should not") assert(times == 1, "computed ran when it should not")
end) end)

View file

@ -1,20 +1,20 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope local effectScope = core.effectScope
local endBatch = signals.endBatch local endBatch = core.endBatch
local setCurrentSub = signals.setCurrentSub local setCurrentSub = core.setCurrentSub
local onEffectCleanup = signals.onEffectCleanup local onCleanup = core.onCleanup
local signal = signals.signal local atom = core.atom
local startBatch = signals.startBatch local startBatch = core.startBatch
local test = suite.test local test = suite.test
test("should clear subscriptions when untracked by all subscribers", function() test("should clear subscriptions when untracked by all subscribers", function()
local bRunTimes = 0 local bRunTimes = 0
local a, setA = signal(1) local a = atom(1)
local b = computed(function() local b = computed(function()
bRunTimes += 1 bRunTimes += 1
return a() * 2 return a() * 2
@ -24,15 +24,15 @@ test("should clear subscriptions when untracked by all subscribers", function()
end) end)
assert(bRunTimes == 1, "should run initially") assert(bRunTimes == 1, "should run initially")
setA(2) a(2)
assert(bRunTimes == 2, "should run after change") assert(bRunTimes == 2, "should run after change")
stopEffect() stopEffect()
setA(3) a(3)
assert(bRunTimes == 2, "should not run after stop") assert(bRunTimes == 2, "should not run after stop")
end) end)
test("should not run untracked inner effect", function() test("should not run untracked inner effect", function()
local a, setA = signal(3) local a = atom(3)
local b = computed(function() local b = computed(function()
return a() > 0 return a() > 0
end) end)
@ -47,14 +47,14 @@ test("should not run untracked inner effect", function()
end end
end) end)
setA(2) a(2)
setA(1) a(1)
setA(0) a(0)
end) end)
test("should run outer effect first", function() test("should run outer effect first", function()
local a, setA = signal(1) local a = atom(1)
local b, setB = signal(1) local b = atom(1)
effect(function() effect(function()
if a() > 0 then if a() > 0 then
@ -68,13 +68,13 @@ test("should run outer effect first", function()
end) end)
startBatch() startBatch()
setB(0) b(0)
setA(0) a(0)
endBatch() endBatch()
end) end)
test("should not trigger inner effect when resolve maybe dirty", function() test("should not trigger inner effect when resolve maybe dirty", function()
local a, setA = signal(0) local a = atom(0)
local b = computed(function() local b = computed(function()
return a() % 2 return a() % 2
end) end)
@ -91,12 +91,12 @@ test("should not trigger inner effect when resolve maybe dirty", function()
end) end)
end) end)
setA(2) a(2)
end) end)
test("should trigger inner effects in sequence", function() test("should trigger inner effects in sequence", function()
local a, setA = signal(0) local a = atom(0)
local b, setB = signal(0) local b = atom(0)
local c = computed(function() local c = computed(function()
return a() - b() return a() - b()
end) end)
@ -120,16 +120,16 @@ test("should trigger inner effects in sequence", function()
table.clear(order) table.clear(order)
startBatch() startBatch()
setB(1) b(1)
setA(1) a(1)
endBatch() endBatch()
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`) assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
end) end)
test("should trigger inner effects in sequence in effect scope", function() test("should trigger inner effects in sequence in effect scope", function()
local a, setA = signal(0) local a = atom(0)
local b, setB = signal(0) local b = atom(0)
local order: { string } = {} local order: { string } = {}
effectScope(function() effectScope(function()
@ -148,8 +148,8 @@ test("should trigger inner effects in sequence in effect scope", function()
table.clear(order) table.clear(order)
startBatch() startBatch()
setB(1) b(1)
setA(1) a(1)
endBatch() endBatch()
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`) 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 end
local logs: { string } = {} local logs: { string } = {}
local a = signal(0) local a = atom(0)
local b, setB = signal(0) local b = atom(0)
local aa = computed(function() local aa = computed(function()
table.insert(logs, "aa-0") table.insert(logs, "aa-0")
if a() == 0 then if a() == 0 then
setB(1) b(1)
end end
table.insert(logs, "aa-1") table.insert(logs, "aa-1")
return nil return nil
@ -196,8 +196,8 @@ test("should custom effect support batch", function()
end) end)
test("should duplicate subscribers do not affect the notify order", function() test("should duplicate subscribers do not affect the notify order", function()
local src1, setSrc1 = signal(0) local src1 = atom(0)
local src2, setSrc2 = signal(0) local src2 = atom(0)
local order: { string } = {} local order: { string } = {}
effect(function() effect(function()
@ -215,17 +215,17 @@ test("should duplicate subscribers do not affect the notify order", function()
table.insert(order, "b") table.insert(order, "b")
src1() src1()
end) end)
setSrc2(1) -- src1.subs: a -> b -> a src2(1) -- src1.subs: a -> b -> a
table.clear(order) table.clear(order)
setSrc1(src1() + 1) src1(src1() + 1)
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`) assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
end) end)
test("should handle side effect with inner effects", function() test("should handle side effect with inner effects", function()
local a, setA = signal(0) local a = atom(0)
local b, setB = signal(0) local b = atom(0)
local order: { string } = {} local order: { string } = {}
effect(function() 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)}`) assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
table.clear(order) table.clear(order)
setB(1) b(1)
setA(1) a(1)
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`) assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
end) end)
end) end)
test("should handle flags are indirectly updated during checkDirty", function() test("should handle flags are indirectly updated during checkDirty", function()
local a, setA = signal(false) local a = atom(false)
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -267,27 +267,27 @@ test("should handle flags are indirectly updated during checkDirty", function()
triggers += 1 triggers += 1
end) end)
assert(triggers == 1, "did not run initially") assert(triggers == 1, "did not run initially")
setA(true) a(true)
assert(triggers == 2, "did not update indirectly") assert(triggers == 2, "did not update indirectly")
end) end)
test("should run cleanups in sequence", function() test("should run cleanups in sequence", function()
local order = {} local order = {}
local a, setA = signal(false) local a = atom(false)
local stopEffect = effect(function() local stopEffect = effect(function()
a() a()
onEffectCleanup(function() onCleanup(function()
table.insert(order, "a") table.insert(order, "a")
end) end)
onEffectCleanup(function() onCleanup(function()
table.insert(order, "b") table.insert(order, "b")
end) end)
end) end)
assert(table.concat(order) == "", "should not cleanup immediately") assert(table.concat(order) == "", "should not cleanup immediately")
setA(true) a(true)
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`) assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
table.clear(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)}`) assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`)
table.clear(order) table.clear(order)
setA(false) a(false)
assert(table.concat(order) == "", "should not cleanup after update") assert(table.concat(order) == "", "should not cleanup after update")
end) end)
test("should run inner effect cleanup", function() test("should run inner effect cleanup", function()
local cleanups = 0 local cleanups = 0
local src, setSrc = signal(false) local src = atom(false)
local stopEffect = effect(function() local stopEffect = effect(function()
src() src()
effect(function() effect(function()
onEffectCleanup(function() onCleanup(function()
cleanups += 1 cleanups += 1
end) end)
end) end)
effect(function() effect(function()
onEffectCleanup(function() onCleanup(function()
cleanups += 1 cleanups += 1
end) end)
end) end)
end) end)
assert(cleanups == 0, "should not cleanup immediately") assert(cleanups == 0, "should not cleanup immediately")
setSrc(true) src(true)
assert(cleanups == 2, "should cleanup after outer update") assert(cleanups == 2, "should cleanup after outer update")
stopEffect() stopEffect()
assert(cleanups == 4, "should cleanup after outer dispose") assert(cleanups == 4, "should cleanup after outer dispose")
setSrc(false) src(false)
assert(cleanups == 4, "should not cleanup after update") assert(cleanups == 4, "should not cleanup after update")
end) end)
test("should run cleanups outside any evaluation context", function() test("should run cleanups outside any evaluation context", function()
local calls = 0 local calls = 0
local a, setA = signal(0) local a = atom(0)
local b, setB = signal(0) local b = atom(0)
local c = computed(function() local c = computed(function()
if a() == 0 then if a() == 0 then
effect(function() effect(function()
onEffectCleanup(function() onCleanup(function()
b() b()
end) end)
end) end)
@ -348,11 +348,11 @@ test("should run cleanups outside any evaluation context", function()
assert(calls == 1, "should run once") assert(calls == 1, "should run once")
calls = 0 calls = 0
setA(1) a(1)
assert(calls == 1, "should run once after a update") assert(calls == 1, "should run once after a update")
calls = 0 calls = 0
setB(1) b(1)
assert(calls == 0, "should not run after b update") assert(calls == 0, "should not run after b update")
end) end)

View file

@ -1,15 +1,14 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope local effectScope = core.effectScope
local signal = signals.signal local atom = core.atom
local onEffectCleanup = signals.onEffectCleanup local onCleanup = core.onCleanup
local onScopeDispose = signals.onScopeDispose
local test = suite.test local test = suite.test
test("should not trigger after stop", function() test("should not trigger after stop", function()
local count, setCount = signal(1) local count = atom(1)
local triggers = 0 local triggers = 0
@ -20,14 +19,14 @@ test("should not trigger after stop", function()
end) end)
assert(triggers == 1, "did not run initially") assert(triggers == 1, "did not run initially")
setCount(2) count(2)
assert(triggers == 2, "did not run after update") assert(triggers == 2, "did not run after update")
end) end)
setCount(3) count(3)
assert(triggers == 3, "did not run after update outside scope") assert(triggers == 3, "did not run after update outside scope")
stopScope() stopScope()
setCount(4) count(4)
assert(triggers == 3, "ran after stop") assert(triggers == 3, "ran after stop")
end) end)
@ -37,7 +36,7 @@ test("should run indirect scope cleanup", function()
local cleanup local cleanup
local stopScope = effectScope(function() local stopScope = effectScope(function()
cleanup = effectScope(function() cleanup = effectScope(function()
onScopeDispose(function() onCleanup(function()
cleanups += 1 cleanups += 1
end) end)
end) end)
@ -53,22 +52,22 @@ end)
test("should run indirect effect cleanup", function() test("should run indirect effect cleanup", function()
local cleanups = 0 local cleanups = 0
local a, setA = signal(false) local a = atom(false)
local stopScope = effectScope(function() local stopScope = effectScope(function()
effect(function() effect(function()
a() a()
onEffectCleanup(function() onCleanup(function()
cleanups += 1 cleanups += 1
end) end)
end) end)
end) end)
assert(cleanups == 0, "should not cleanup immediately") assert(cleanups == 0, "should not cleanup immediately")
setA(true) a(true)
assert(cleanups == 1, "should cleanup once") assert(cleanups == 1, "should cleanup once")
stopScope() stopScope()
assert(cleanups == 2, "should cleanup after outer dispose") assert(cleanups == 2, "should cleanup after outer dispose")
setA(false) a(false)
assert(cleanups == 2, "should not cleanup after update") assert(cleanups == 2, "should not cleanup after update")
end) end)

View file

@ -1,10 +1,10 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local signal = signals.signal local atom = core.atom
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope local effectScope = core.effectScope
local test = suite.test local test = suite.test
--- Creates a weak reference to a value. --- Creates a weak reference to a value.
@ -23,13 +23,13 @@ local function gc()
end end
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 --- parent scope is disposed. Calling a computed value outside of a scope links
--- it to dependencies, but the dependencies hold strong references to the --- it to dependencies, but the dependencies hold strong references to the
--- computed value, so it won't GC until called in a scope and disposed. --- computed value, so it won't GC until called in a scope and disposed.
--- ---
--- This function releases a computed value by linking it to a scope and then --- This function releases a computed value by linking it to a scope and then
--- immediately closing it, causing alien-signals to unlink the computed value --- immediately closing it, causing alien-core to unlink the computed value
--- and allow it to be garbage collected. --- and allow it to be garbage collected.
local function release(getter: () -> ()) local function release(getter: () -> ())
effectScope(function() effectScope(function()
@ -39,23 +39,23 @@ end
test("should release computed cache", function() test("should release computed cache", function()
local srcRef = weakRef({}) local srcRef = weakRef({})
local src, setSrc = signal(srcRef()) local src = atom(srcRef())
local c = computed(function() local c = computed(function()
return src() return src()
end) end)
c() -- cache src value c() -- cache src value
setSrc(nil) -- release value src(nil) -- release value
c = release(c) -- release computed c = release(c) -- release computed
gc() gc()
assert(srcRef() == nil, "should release computed cache") assert(srcRef() == nil, "should release computed cache")
end) end)
test("should not release effect for signal", function() test("should not release effect for atom", function()
local effectCalls = 0 local effectCalls = 0
local src, setSrc = signal(0) local src = atom(0)
effect(function() effect(function()
effectCalls += 1 effectCalls += 1
@ -65,7 +65,7 @@ test("should not release effect for signal", function()
assert(effectCalls == 1, "should call effect once") assert(effectCalls == 1, "should call effect once")
gc() gc()
setSrc(src() + 1) src(src() + 1)
assert(effectCalls == 2, "should call effect again") assert(effectCalls == 2, "should call effect again")
end) end)

View file

@ -1,10 +1,10 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local setCurrentSub = signals.setCurrentSub local setCurrentSub = core.setCurrentSub
local signal = signals.signal local atom = core.atom
local test = suite.test local test = suite.test
type ReactionOptions<T = unknown> = { type ReactionOptions<T = unknown> = {
@ -90,7 +90,7 @@ local function reaction<T>(
end end
test("#48", function() test("#48", function()
local source, setSource = signal(0) local source = atom(0)
local disposeInner: () -> () local disposeInner: () -> ()
reaction(function() reaction(function()
@ -105,9 +105,9 @@ test("#48", function()
end end
end) end)
setSource(1) source(1)
setSource(2) source(2)
setSource(3) source(3)
end) end)
return {} return {}

View file

@ -1,15 +1,15 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local signal = signals.signal local atom = core.atom
local describe = suite.describe local describe = suite.describe
local test = suite.test local test = suite.test
--[[ --[[
Tests adopted with thanks from preact-signals implementation at Tests adopted with thanks from preact-core implementation at
https://github.com/preactjs/signals/blob/main/packages/core/test/signal.test.tsx https://github.com/preactjs/core/blob/main/packages/core/test/atom.test.tsx
The MIT License (MIT) The MIT License (MIT)
@ -43,7 +43,7 @@ describe("graph updates", function()
-- C -- C
-- | -- |
-- D -- D
local a, setA = signal(2) local a = atom(2)
local b = computed(function() local b = computed(function()
return a() - 1 return a() - 1
@ -64,12 +64,12 @@ describe("graph updates", function()
assert(computeCalls == 1, "compute should be called once") assert(computeCalls == 1, "compute should be called once")
computeCalls = 0 computeCalls = 0
setA(4) a(4)
d() d()
assert(computeCalls == 1, "compute should drop update") assert(computeCalls == 1, "compute should drop update")
end) 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 -- In this scenario "D" should only update once when "A" receives
-- an update. This is sometimes referred to as the "diamond" scenario. -- an update. This is sometimes referred to as the "diamond" scenario.
-- A -- A
@ -78,7 +78,7 @@ describe("graph updates", function()
-- \ / -- \ /
-- D -- D
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -96,12 +96,12 @@ describe("graph updates", function()
assert(d() == "a a", "d should be 'a a'") assert(d() == "a a", "d should be 'a a'")
assert(spyCalls == 1, "compute should be called once") assert(spyCalls == 1, "compute should be called once")
setA("aa") a("aa")
assert(d() == "aa aa", "d should be 'aa aa'") assert(d() == "aa aa", "d should be 'aa aa'")
assert(spyCalls == 2, "compute should be called only twice") assert(spyCalls == 2, "compute should be called only twice")
end) 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. -- "E" will be likely updated twice if our mark+sweep logic is buggy.
-- A -- A
-- / \ -- / \
@ -111,7 +111,7 @@ describe("graph updates", function()
-- | -- |
-- E -- E
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -133,7 +133,7 @@ describe("graph updates", function()
assert(e() == "a a", "e should be 'a a'") assert(e() == "a a", "e should be 'a a'")
assert(spyCalls == 1, "compute should be called once") assert(spyCalls == 1, "compute should be called once")
setA("aa") a("aa")
assert(e() == "aa aa", "e should be 'aa aa'") assert(e() == "aa aa", "e should be 'aa aa'")
assert(spyCalls == 2, "compute should be called only twice") assert(spyCalls == 2, "compute should be called only twice")
end) end)
@ -141,7 +141,7 @@ describe("graph updates", function()
test("should bail out if result is the same", function() test("should bail out if result is the same", function()
-- Bail out if value of "B" never changes -- Bail out if value of "B" never changes
-- A->B->C -- A->B->C
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
a() a()
return "foo" return "foo"
@ -157,12 +157,12 @@ describe("graph updates", function()
assert(c() == "foo", "c should be 'foo'") assert(c() == "foo", "c should be 'foo'")
assert(spyCalls == 1, "compute should be called once") assert(spyCalls == 1, "compute should be called once")
setA("aa") a("aa")
assert(c() == "foo", "c should be 'foo'") assert(c() == "foo", "c should be 'foo'")
assert(spyCalls == 1, "compute should bail out") assert(spyCalls == 1, "compute should bail out")
end) 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. -- "F" and "G" will be likely updated twice if our mark+sweep logic is buggy.
-- A -- A
-- / \ -- / \
@ -173,7 +173,7 @@ describe("graph updates", function()
-- E -- E
-- / \ -- / \
-- F G -- F G
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
@ -222,7 +222,7 @@ describe("graph updates", function()
fSpyCalls, fSpyTime = 0, 0 fSpyCalls, fSpyTime = 0, 0
gSpyCalls, gSpyTime = 0, 0 gSpyCalls, gSpyTime = 0, 0
setA("b") a("b")
assert(e() == "b b", "e should be 'b b'") assert(e() == "b b", "e should be 'b b'")
assert(eSpyCalls == 1, "e should run once after update") assert(eSpyCalls == 1, "e should run once after update")
@ -237,7 +237,7 @@ describe("graph updates", function()
fSpyCalls, fSpyTime = 0, 0 fSpyCalls, fSpyTime = 0, 0
gSpyCalls, gSpyTime = 0, 0 gSpyCalls, gSpyTime = 0, 0
setA("c") a("c")
assert(e() == "c c", "e should be 'c c'") assert(e() == "c c", "e should be 'c c'")
assert(eSpyCalls == 1, "e should run once after update") 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") assert(eSpyTime < gSpyTime, "e should be called before g")
end) end)
test("should only subscribe to signals listened to", function() test("should only subscribe to core listened to", function()
-- *A -- *A
-- / \ -- / \
-- *B C <- we don't listen to C -- *B C <- we don't listen to C
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
@ -273,12 +273,12 @@ describe("graph updates", function()
assert(b() == "a", "b should be 'a'") assert(b() == "a", "b should be 'a'")
assert(spyCalls == 0, "compute should not run") assert(spyCalls == 0, "compute should not run")
setA("aa") a("aa")
assert(b() == "aa", "b should be 'aa'") assert(b() == "aa", "b should be 'aa'")
assert(spyCalls == 0, "compute should not run") assert(spyCalls == 0, "compute should not run")
end) 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 -- Here both "B" and "C" are active in the beginning, but
-- "B" becomes inactive later. At that point it should -- "B" becomes inactive later. At that point it should
-- not receive any updates anymore. -- not receive any updates anymore.
@ -287,7 +287,7 @@ describe("graph updates", function()
-- *B D <- we don't listen to C -- *B D <- we don't listen to C
-- | -- |
-- *C -- *C
local a, setA = signal("a") local a = atom("a")
local spyBCalls = 0 local spyBCalls = 0
local spyB = function() local spyB = function()
spyBCalls += 1 spyBCalls += 1
@ -318,7 +318,7 @@ describe("graph updates", function()
spyCCalls = 0 spyCCalls = 0
unsub() unsub()
setA("aa") a("aa")
assert(spyBCalls == 0, "b should not run") assert(spyBCalls == 0, "b should not run")
assert(spyCCalls == 0, "c 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 -- B *C <- returns same value every time
-- \ / -- \ /
-- D -- D
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -353,7 +353,7 @@ describe("graph updates", function()
assert(d() == "a c", "d should be 'a c'") assert(d() == "a c", "d should be 'a c'")
spyResult = nil spyResult = nil
setA("aa") a("aa")
d() d()
assert(spyResult == "aa c", "d should have returned 'aa c'") assert(spyResult == "aa c", "d should have returned 'aa c'")
end) end)
@ -367,7 +367,7 @@ describe("graph updates", function()
-- B *C *D -- B *C *D
-- \ | / -- \ | /
-- E -- E
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -389,13 +389,13 @@ describe("graph updates", function()
assert(e() == "a c d", "e should be 'a c d'") assert(e() == "a c d", "e should be 'a c d'")
spyResult = nil spyResult = nil
setA("aa") a("aa")
e() e()
assert(spyResult == "aa c d", "e should have returned 'aa c d'") assert(spyResult == "aa c d", "e should have returned 'aa c d'")
end) end)
test("should support lazy branches", function() test("should support lazy branches", function()
local a, setA = signal(0) local a = atom(0)
local b = computed(function() local b = computed(function()
return a() return a()
end) end)
@ -404,10 +404,10 @@ describe("graph updates", function()
end) end)
assert(c() == 0, "c should be 0") assert(c() == 0, "c should be 0")
setA(1) a(1)
assert(c() == 1, "c should be 1") assert(c() == 1, "c should be 1")
setA(0) a(0)
assert(c() == 0, "c should be 0 again") assert(c() == 0, "c should be 0 again")
end) end)
@ -419,7 +419,7 @@ describe("graph updates", function()
-- *B *C -- *B *C
-- \ / -- \ /
-- D -- D
local a, setA = signal("a") local a = atom("a")
local b = computed(function() local b = computed(function()
a() a()
return "b" return "b"
@ -438,14 +438,14 @@ describe("graph updates", function()
assert(d() == "b c", "d should be 'b c'") assert(d() == "b c", "d should be 'b c'")
spyCalls = 0 spyCalls = 0
setA("aa") a("aa")
assert(spyCalls == 0, "d should not run") assert(spyCalls == 0, "d should not run")
end) end)
end) end)
describe("error handling", function() describe("error handling", function()
test("should keep graph consistent on errors during activation", function() test("should keep graph consistent on errors during activation", function()
local a, setA = signal(0) local a = atom(0)
local b = computed(function() local b = computed(function()
error("fail") error("fail")
end) end)
@ -459,12 +459,12 @@ describe("error handling", function()
assert(not success, "should throw") assert(not success, "should throw")
assert(string.match(result, "fail"), "should throw fail") assert(string.match(result, "fail"), "should throw fail")
setA(1) a(1)
assert(c() == 1, "c should recover and return 1") assert(c() == 1, "c should recover and return 1")
end) end)
test("should keep graph consistent on errors in computeds", function() test("should keep graph consistent on errors in computeds", function()
local a, setA = signal(0) local a = atom(0)
local b = computed(function() local b = computed(function()
if a() == 1 then if a() == 1 then
error("fail") error("fail")
@ -477,14 +477,14 @@ describe("error handling", function()
assert(c() == 0, "c should be 0") assert(c() == 0, "c should be 0")
setA(1) a(1)
local success, result = pcall(function() local success, result = pcall(function()
b() b()
end) end)
assert(not success, "should throw") assert(not success, "should throw")
assert(string.match(result, "fail"), "should throw fail") assert(string.match(result, "fail"), "should throw fail")
setA(2) a(2)
assert(c() == 2, "c should recover and return 2") assert(c() == 2, "c should recover and return 2")
end) end)
end) end)

View file

@ -1,13 +1,13 @@
local signals = require("../../src/signals") local core = require("../../core")
local suite = require("@test/suite") local suite = require("../../../../../test/suite")
local computed = signals.computed local computed = core.computed
local setCurrentSub = signals.setCurrentSub local setCurrentSub = core.setCurrentSub
local signal = signals.signal local atom = core.atom
local test = suite.test local test = suite.test
test("should pause tracking", function() test("should pause tracking", function()
local src, setSrc = signal(0) local src = atom(0)
local c = computed(function() local c = computed(function()
local currentSub = setCurrentSub(nil) local currentSub = setCurrentSub(nil)
local value = src() local value = src()
@ -16,7 +16,7 @@ test("should pause tracking", function()
end) end)
assert(c() == 0, "should not track src") assert(c() == 0, "should not track src")
setSrc(1) src(1)
assert(c() == 0, "should not track src") assert(c() == 0, "should not track src")
end) end)

View file

@ -1,5 +1,5 @@
local Charm = require("../src") local Charm = require("../")
local suite = require("@test/suite") local suite = require("../../../../test/suite")
local atom = Charm.atom local atom = Charm.atom
local listen = Charm.listen local listen = Charm.listen

View file

@ -1,5 +1,5 @@
local Charm = require("../src") local Charm = require("../")
local suite = require("@test/suite") local suite = require("../../../../test/suite")
local atom = Charm.atom local atom = Charm.atom
local mapped = Charm.mapped local mapped = Charm.mapped

View file

@ -1,5 +1,5 @@
local Charm = require("../src") local Charm = require("../")
local suite = require("@test/suite") local suite = require("../../../../test/suite")
local atom = Charm.atom local atom = Charm.atom
local computed = Charm.computed local computed = Charm.computed

View 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 {}

View file

@ -1,5 +1,5 @@
local Charm = require("../src") local Charm = require("../")
local suite = require("@test/suite") local suite = require("../../../../test/suite")
type Atom<T> = Charm.Atom<T> type Atom<T> = Charm.Atom<T>

View file

@ -1,5 +1,5 @@
local Charm = require("../src") local Charm = require("../")
local suite = require("@test/suite") local suite = require("../../../../test/suite")
local atom = Charm.atom local atom = Charm.atom
local computed = Charm.computed local computed = Charm.computed

View file

@ -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

View file

@ -1,16 +1,14 @@
local signals = require("./signals") local core = require("./core")
type ReactiveNode = signals.ReactiveNode local startBatch = core.startBatch
local endBatch = core.endBatch
local startBatch = signals.startBatch
local endBatch = signals.endBatch
local function handleBatched(ok: boolean, ...) local function handleBatched(ok: boolean, ...)
endBatch() endBatch()
if ok then if ok then
return ... return ...
else else
error(...) error((...))
end end
end end

View file

@ -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/Nicell/alien-signals-luau
https://github.com/stackblitz/alien-signals https://github.com/stackblitz/alien-signals
@ -26,8 +26,9 @@
SOFTWARE. SOFTWARE.
]] ]]
local noYield = require("./noYield") local strict = require("./strict")
local system = require("./system") local system = require("./system")
local utils = require("./utils")
export type ReactiveFlags = system.ReactiveFlags export type ReactiveFlags = system.ReactiveFlags
export type ReactiveNode = system.ReactiveNode export type ReactiveNode = system.ReactiveNode
@ -35,6 +36,7 @@ export type Link = system.Link
local ReactiveFlags = system.ReactiveFlags local ReactiveFlags = system.ReactiveFlags
local createReactiveSystem = system.createReactiveSystem local createReactiveSystem = system.createReactiveSystem
local noYield = utils.noYield
local NONE = 0b0000000 local NONE = 0b0000000
local MUTABLE = 0b0000001 local MUTABLE = 0b0000001
@ -48,23 +50,22 @@ type EffectScope = ReactiveNode & {
} }
type Effect = ReactiveNode & { type Effect = ReactiveNode & {
fn: () -> (), fn: () -> ...(() -> ())?,
cleanups: { () -> () }, cleanups: { () -> () },
} }
type Computed<T = any> = ReactiveNode & { type Computed<T = any> = ReactiveNode & {
value: T?, value: T?,
getter: (previousValue: T?) -> T, getter: (previousValue: T?) -> T,
equals: ((T, T) -> boolean)?,
} }
type Signal<T = any> = ReactiveNode & { type Atom<T = any> = ReactiveNode & {
previousValue: T, previousValue: T,
value: T, value: T,
equals: ((T, T) -> boolean)?,
} }
local pauseStack: { ReactiveNode? } = {}
local queuedEffects: { Effect | EffectScope? } = {}
local link: (dep: ReactiveNode, sub: ReactiveNode) -> () local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
local unlink: (link: Link, sub: ReactiveNode?) -> Link? local unlink: (link: Link, sub: ReactiveNode?) -> Link?
local propagate: (link: Link) -> () local propagate: (link: Link) -> ()
@ -74,9 +75,10 @@ local startTracking: (sub: ReactiveNode) -> ()
local shallowPropagate: (link: Link) -> () local shallowPropagate: (link: Link) -> ()
local batchDepth = 0 local batchDepth = 0
local queuedEffects: { Effect | EffectScope? } = {}
local notifyIndex = 0
local queuedEffectsLength = 0 local queuedEffectsLength = 0
local notifyIndex = 0
local activeSubTracking = true
local activeSub: ReactiveNode? local activeSub: ReactiveNode?
local activeScope: Effect | EffectScope? local activeScope: Effect | EffectScope?
@ -100,50 +102,58 @@ local function setCurrentScope(scope: Effect | EffectScope?)
return prevScope return prevScope
end 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 if activeSub then
table.insert((activeSub :: Effect).cleanups, cleanup) table.insert((activeSub :: Effect).cleanups, cleanup)
end elseif activeScope then
end
local function onScopeDispose(cleanup: () -> ())
if activeScope then
table.insert(activeScope.cleanups, cleanup) 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
end end
local function cleanupEffect(e: Effect | EffectScope) local function run(effect: Effect | EffectScope, flags: ReactiveFlags): ()
for _, cleanup in e.cleanups :: { () -> () } do if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
local prevSub = setCurrentSub(nil) runCleanups(effect)
local success, result: unknown = noYield(cleanup) local prev = setCurrentSub(effect)
setCurrentSub(prevSub) local prevTracking = setCurrentTracking(true)
if not success then startTracking(effect)
error(result) local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn)
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)
setCurrentSub(prev) setCurrentSub(prev)
endTracking(e) endTracking(effect)
setCurrentTracking(prevTracking)
if not success then if not success then
error(result) error(result)
elseif result then
table.insert(effect.cleanups, result)
end end
return return
elseif bit32.band(flags, PENDING) ~= 0 then elseif bit32.btest(flags, PENDING) then
(e :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING)) (effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
end end
local link = e.deps local link = effect.deps
while link do while link do
local dep = link.dep local dep = link.dep
local depFlags = dep.flags local depFlags = dep.flags
if bit32.band(depFlags, QUEUED) ~= 0 then if bit32.btest(depFlags, QUEUED) then
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED)) depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
dep.flags = depFlags dep.flags = depFlags
run(dep :: Effect, depFlags) run(dep :: Effect, depFlags)
@ -179,209 +189,154 @@ local function endBatch()
end end
end end
--- @deprecated Will be removed in the next major version. Use local function updateComputed(computed: Computed): boolean
--- `const pausedSub = setCurrentSub(undefined)` instead for better performance. local prevSub = setCurrentSub(computed)
local function pauseTracking() local prevTracking = setCurrentTracking(true)
table.insert(pauseStack, setCurrentSub(nil)) startTracking(computed)
end local oldValue = computed.value
local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue)
--- @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)
setCurrentSub(prevSub) setCurrentSub(prevSub)
endTracking(c) endTracking(computed)
setCurrentTracking(prevTracking)
if not success then if not success then
error(result) error(result)
end end
if oldValue ~= result then local equals = computed.equals
c.value = result if if equals then not equals(oldValue, result) else oldValue ~= result then
computed.value = result
return true return true
else else
return false return false
end end
end end
local function updateSignal(s: Signal, value: any): boolean local function updateAtom(atom: Atom, value: any): boolean
s.flags = MUTABLE atom.flags = MUTABLE
if s.previousValue ~= value then if atom.previousValue ~= value then
s.previousValue = value atom.previousValue = value
return true return true
else else
return false return false
end end
end end
local function signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) -> T) | T) -> T) local function readAtom<T>(atom: Atom<T>): T
local function getter(): T local value = atom.value
local value = this.value if bit32.btest(atom.flags, DIRTY) then
if bit32.band(this.flags, DIRTY) ~= 0 then if updateAtom(atom, value) then
if updateSignal(this, value) then local subs = atom.subs
local subs = this.subs
if subs then if subs then
shallowPropagate(subs) shallowPropagate(subs)
end end
end end
end end
if activeSub then if activeSub and activeSubTracking then
link(this, activeSub) link(atom, activeSub)
end end
return value return value
end end
local function setter(newValue: ((T) -> T) | T): T local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
local value = this.value local equals = atom.equals
local newValue = newValue :: T if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then
if type(newValue) == "function" then atom.value = newValue
local prev = setCurrentSub(nil) atom.flags = bit32.bor(MUTABLE, DIRTY)
local success, result = noYield(newValue, value) local subs = atom.subs
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 if subs then
propagate(subs) propagate(subs)
if batchDepth == 0 then if batchDepth == 0 then
flush() flush()
end end
end end
end
return newValue return newValue
end end
return oldValue
return getter, setter
end end
local function computedGetter<T>(this: Computed<T>): () -> T local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
return function() return function(...)
local flags = this.flags if select("#", ...) == 0 then
if return readAtom(atom)
bit32.band(flags, DIRTY) ~= 0
or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this))
then
if updateComputed(this) then
local subs = this.subs
if subs then
shallowPropagate(subs)
end end
end local oldValue = atom.value
elseif bit32.band(flags, PENDING) ~= 0 then local newValue = ...
this.flags = bit32.band(flags, bit32.bnot(PENDING)) if type(newValue) == "function" then
end local prev = setCurrentSub(nil)
if activeSub then local success, result = (if strict.enabled then noYield else pcall)(newValue, oldValue)
link(this, activeSub)
elseif activeScope then
link(this, activeScope)
end
return this.value :: T
end
end
local function effectStop(this: Effect | EffectScope): () -> ()
return function()
local dep = this.deps
while dep do
dep = unlink(dep, this)
end
local sub = this.subs
if sub then
unlink(sub)
end
(this :: Effect).flags = NONE
cleanupEffect(this)
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) setCurrentSub(prev)
if not success then if not success then
error(result) error(result)
end end
return effectStop(e) newValue = result
end
return writeAtom(atom, oldValue, newValue :: T)
end
end end
local function effectScope(fn: () -> (), detached: boolean?): () -> () local function computedGetter<T>(computed: Computed<T>): () -> T
local e: EffectScope = { return function()
cleanups = {}, local flags = computed.flags
flags = NONE, if
} bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
if activeScope and not detached then then
link(e, activeScope) if updateComputed(computed) then
local subs = computed.subs
if subs then
shallowPropagate(subs)
end 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 end
return effectStop(e) 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 end
local function update(signal: Signal | Computed): boolean local function effectStop(self: Effect | EffectScope): () -> ()
if (signal :: Computed).getter then return function()
return updateComputed(signal :: Computed) 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 else
return updateSignal(signal :: Signal, signal.value) return updateAtom(atom :: Atom, atom.value)
end end
end end
local function notify(e: Effect | EffectScope) local function notify(effect: Effect | EffectScope)
local flags = e.flags local flags = effect.flags
if bit32.band(flags, QUEUED) == 0 then if not bit32.btest(flags, QUEUED) then
(e :: Effect).flags = bit32.bor(flags, QUEUED) (effect :: Effect).flags = bit32.bor(flags, QUEUED)
local subs = e.subs local subs = effect.subs
if subs then if subs then
notify(subs.sub :: Effect) notify(subs.sub :: Effect)
else else
queuedEffectsLength += 1 queuedEffectsLength += 1
queuedEffects[queuedEffectsLength] = e queuedEffects[queuedEffectsLength] = effect
end end
end end
end end
local function unwatched(node: Signal | Computed | Effect | EffectScope) local function unwatched(node: Atom | Computed | Effect | EffectScope)
if (node :: Computed).getter then if (node :: Computed).getter then
local toRemove = node.deps local toRemove = node.deps
if toRemove then if toRemove then
@ -390,11 +345,73 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope)
toRemove = unlink(toRemove, node) toRemove = unlink(toRemove, node)
until not toRemove until not toRemove
end end
elseif (node :: Effect).cleanups ~= nil then elseif (node :: Effect).cleanups then
effectStop(node :: Effect)() effectStop(node :: Effect)()
end end
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({ local reactiveSystem = createReactiveSystem({
update = update :: (ReactiveNode) -> boolean, update = update :: (ReactiveNode) -> boolean,
notify = notify :: (ReactiveNode) -> (), notify = notify :: (ReactiveNode) -> (),
@ -416,14 +433,12 @@ return {
setCurrentSub = setCurrentSub, setCurrentSub = setCurrentSub,
getCurrentScope = getCurrentScope, getCurrentScope = getCurrentScope,
setCurrentScope = setCurrentScope, setCurrentScope = setCurrentScope,
setCurrentTracking = setCurrentTracking,
onCleanup = onCleanup,
getBatchDepth = getBatchDepth, getBatchDepth = getBatchDepth,
startBatch = startBatch, startBatch = startBatch,
endBatch = endBatch, endBatch = endBatch,
pauseTracking = pauseTracking, atom = atom,
resumeTracking = resumeTracking,
onEffectCleanup = onEffectCleanup,
onScopeDispose = onScopeDispose,
signal = signal,
computed = computed, computed = computed,
effect = effect, effect = effect,
effectScope = effectScope, effectScope = effectScope,

View file

@ -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

View file

@ -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
View file

@ -0,0 +1 @@
export {};

View file

@ -1,25 +1,25 @@
local atom = require("@self/atom")
local batched = require("@self/batched") local batched = require("@self/batched")
local effect = require("@self/effect") local core = require("@self/core")
local listen = require("@self/listen") local listen = require("@self/listen")
local mapped = require("@self/mapped") local mapped = require("@self/mapped")
local observe = require("@self/observe") local observe = require("@self/observe")
local onCleanup = require("@self/onCleanup") local strict = require("@self/strict")
local signals = require("@self/signals")
local subscribe = require("@self/subscribe") local subscribe = require("@self/subscribe")
local types = require("@self/types") local types = require("@self/types")
local untracked = require("@self/untracked") local untracked = require("@self/untracked")
export type Atom<T = any> = types.Atom<T> 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 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 atom = core.atom
local effectScope = signals.effectScope local computed = core.computed
local effect = core.effect
local effectScope = core.effectScope
local onCleanup = core.onCleanup
return { return {
core = core,
strict = strict,
atom = atom, atom = atom,
batched = batched, batched = batched,
computed = computed, computed = computed,
@ -28,8 +28,7 @@ return {
listen = listen, listen = listen,
mapped = mapped, mapped = mapped,
observe = observe, observe = observe,
onCleanup = onCleanup,
subscribe = subscribe, subscribe = subscribe,
untracked = untracked, untracked = untracked,
onCleanup = onCleanup,
signals = signals,
} }

View file

@ -1,9 +1,8 @@
local signals = require("./signals") local core = require("./core")
local untracked = require("./untracked")
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope
local onEffectCleanup = signals.onEffectCleanup
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> () local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
local tracked = computed(function() local tracked = computed(function()
@ -20,12 +19,7 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
dispose = effect(function() dispose = effect(function()
local previous = current local previous = current
current = tracked() current = tracked()
untracked(listener, current, previous, dispose)
local stopScope = effectScope(function()
listener(current, previous, dispose)
end)
onEffectCleanup(stopScope)
end) end)
if disposed then if disposed then

View file

@ -1,35 +1,26 @@
local noYield = require("./noYield") local core = require("./core")
local signals = require("./signals")
local computed = signals.computed local computed = core.computed
local setCurrentSub = signals.setCurrentSub local setCurrentSub = core.setCurrentSub
type Mapped = type Mapped =
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO }) & (<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 }) & (<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 function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
local currentData: { [KI]: VI } = {} local currentData: { [KI]: VI } = {}
local keyCache: { [KI]: KO } = {} local keyCache: { [KI]: KO } = {}
local function map(value: VI, key: KI): (VO, KO) local function map(value: VI, key: KI): (VO, KO)
local currentSub = setCurrentSub(nil) local currentSub = setCurrentSub(nil)
local success, valueOut, keyOut = noYield(mapper, value, key) local success, valueOut, keyOut = pcall(mapper, value, key)
setCurrentSub(currentSub) setCurrentSub(currentSub)
if not success then if not success then
error(`mapped() transform function failed: {valueOut}`) error(`mapped() transform function failed: {valueOut}`)
end end
if keyOut == nil then return valueOut, if keyOut == nil then key else keyOut :: any
keyOut = key :: any
end
return valueOut, keyOut
end end
return computed(function(previousOutput) 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 if currentData[key] ~= nil then
continue continue
end end
local cachedKey = keyCache[key] local cachedKey = keyCache[key]
if cachedKey then if cachedKey then
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput) currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
currentOutput[cachedKey] = nil currentOutput[cachedKey] = nil
keyCache[key] = nil keyCache[key] = nil
end end
@ -55,11 +44,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
if value == previousData[key] then if value == previousData[key] then
continue continue
end end
local valueOut, keyOut = map(value, key) local valueOut, keyOut = map(value, key)
if currentOutput[keyOut] ~= valueOut then if currentOutput[keyOut] ~= valueOut then
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput) currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
currentOutput[keyOut] = valueOut currentOutput[keyOut] = valueOut
keyCache[key] = keyOut keyCache[key] = keyOut
end end

View file

@ -1,9 +1,8 @@
local onCleanup = require("./onCleanup") local core = require("./core")
local signals = require("./signals")
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope local effectScope = core.effectScope
local onScopeDispose = signals.onScopeDispose local onCleanup = core.onCleanup
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> () local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
local scopes: { [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 }) local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
for key in previousItems do for key in previousItems do
if currentItems[key] == nil and scopes[key] then local dispose = scopes[key]
scopes[key]() if currentItems[key] == nil and dispose then
dispose()
scopes[key] = nil scopes[key] = nil
end end
end end
@ -20,10 +20,7 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
for key, value in currentItems do for key, value in currentItems do
if previousItems[key] == nil then if previousItems[key] == nil then
scopes[key] = effectScope(function() scopes[key] = effectScope(function()
local cleanup = observer(value, key) return observer(value, key)
if cleanup then
onScopeDispose(cleanup)
end
end, true) end, true)
end end
end end
@ -39,7 +36,6 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
local disposeEffect = effect(function() local disposeEffect = effect(function()
local previousItems = currentItems local previousItems = currentItems
currentItems = selector() currentItems = selector()
if currentItems ~= previousItems then if currentItems ~= previousItems then
updateScopes(currentItems, previousItems) updateScopes(currentItems, previousItems)
end end

View file

@ -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

View file

@ -0,0 +1,8 @@
local function inlineTest(): string
return debug.info(1, "n")
end
return {
-- Enable strict mode in O2
enabled = inlineTest() == "inlineTest",
}

View file

@ -1,9 +1,8 @@
local signals = require("./signals") local core = require("./core")
local untracked = require("./untracked")
local computed = signals.computed local computed = core.computed
local effect = signals.effect local effect = core.effect
local effectScope = signals.effectScope
local onEffectCleanup = signals.onEffectCleanup
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> () local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
local tracked = computed(function() local tracked = computed(function()
@ -16,17 +15,11 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
return effect(function() return effect(function()
local previous = current local previous = current
current = tracked() current = tracked()
if mounted then if mounted then
mounted = false mounted = false
return else
untracked(listener, current, previous)
end end
local stopScope = effectScope(function()
listener(current, previous)
end)
onEffectCleanup(stopScope)
end) end)
end end

View file

@ -100,7 +100,7 @@ local function createReactiveSystem(props: {
return return
end end
local nextDep: Link? local nextDep: Link?
local recursedCheck = bit32.band(sub.flags, RECURSED_CHECK) ~= 0 local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK)
if recursedCheck then if recursedCheck then
nextDep = if prevDep then prevDep.nextDep else sub.deps nextDep = if prevDep then prevDep.nextDep else sub.deps
if nextDep and nextDep.dep == dep then if nextDep and nextDep.dep == dep then
@ -178,14 +178,14 @@ local function createReactiveSystem(props: {
local flags = sub.flags local flags = sub.flags
if bit32.band(flags, bit32.bor(MUTABLE, WATCHING)) ~= 0 then if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
sub.flags = bit32.bor(flags, PENDING) 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 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) 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)) sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
flags = bit32.band(flags, MUTABLE) flags = bit32.band(flags, MUTABLE)
else else
@ -193,11 +193,11 @@ local function createReactiveSystem(props: {
flags = NONE flags = NONE
end end
if bit32.band(flags, WATCHING) ~= 0 then if bit32.btest(flags, WATCHING) then
notify(sub) notify(sub)
end end
if bit32.band(flags, MUTABLE) ~= 0 then if bit32.btest(flags, MUTABLE) then
local subSubs = sub.subs local subSubs = sub.subs
if subSubs then if subSubs then
link = subSubs link = subSubs
@ -257,7 +257,7 @@ local function createReactiveSystem(props: {
local subFlags = sub.flags local subFlags = sub.flags
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then
sub.flags = bit32.bor(subFlags, DIRTY) sub.flags = bit32.bor(subFlags, DIRTY)
if bit32.band(subFlags, WATCHING) ~= 0 then if bit32.btest(subFlags, WATCHING) then
notify(sub) notify(sub)
end end
end end
@ -275,7 +275,7 @@ local function createReactiveSystem(props: {
local dirty = false local dirty = false
if bit32.band(sub.flags, DIRTY) ~= 0 then if bit32.btest(sub.flags, DIRTY) then
dirty = true dirty = true
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
if update(dep) then if update(dep) then

View file

@ -1,11 +1,5 @@
export type Getter<T = any> = () -> T type Update<T> = ((previous: T) -> T) | T
export type Atom<T> = (...Update<T>) -> T
export type Setter<T = any> = (update: Update<T>) -> T export type Selector<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
return {} return {}

View file

@ -1,21 +1,18 @@
local noYield = require("./noYield") local core = require("./core")
local signals = require("./signals")
type ReactiveNode = signals.ReactiveNode local setCurrentTracking = core.setCurrentTracking
local setCurrentSub = signals.setCurrentSub local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
setCurrentTracking(prevTracking)
local function handleUntracked(currentSub: ReactiveNode?, ok: boolean, ...)
setCurrentSub(currentSub)
if ok then if ok then
return ... return ...
else else
error(...) error((...))
end end
end end
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results... local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
return handleUntracked(setCurrentSub(nil), noYield(callback, ...)) return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
end end
return untracked return untracked

View file

@ -1,13 +1,11 @@
local function handleNoYield(thread: thread, ok: boolean, ...) local function handleNoYield(thread: thread, ok: boolean, ...)
if not ok then if not ok then
return false, debug.traceback(thread, tostring((...))) return false, debug.traceback(thread, tostring((...)))
end elseif coroutine.status(thread) ~= "dead" then
if coroutine.status(thread) ~= "dead" then
return false, debug.traceback(thread, "Attempted to yield!") return false, debug.traceback(thread, "Attempted to yield!")
end else
return true, ... return true, ...
end
end end
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...) 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, ...)) return handleNoYield(thread, coroutine.resume(thread, ...))
end end
-- To eliminate the overhead of noYield returning callback(...) in production, return {
-- replace it with pcall to simulate the same behavior. noYield = noYield :: typeof(pcall),
return if _G.__DEV__ then noYield else pcall :: never }

View file

@ -0,0 +1,6 @@
{
"name": "charm",
"tree": {
"$path": "src"
}
}

View 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

View 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,
}

View 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,
}

View 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

View 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

View 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

View file

@ -0,0 +1,8 @@
local function inlineTest(): string
return debug.info(1, "n")
end
return {
-- Enable strict mode in O2
enabled = inlineTest() == "inlineTest",
}

View 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

View 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,
}

View 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 {}

View 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

View 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),
}

View 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]

View file

@ -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 {}

View file

@ -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 {}

View file

@ -3,6 +3,6 @@ name = "littensy/charm"
version = "0.10.0" version = "0.10.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
exclude = ["tests"] exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"]
[dependencies] [dependencies]

View 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

View 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)

View file

@ -0,0 +1 @@
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").ReactRoblox)

View file

@ -1,5 +1,5 @@
{ {
"name": "ReactCharm", "name": "react-charm",
"tree": { "tree": {
"$path": "src" "$path": "src"
} }

View 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": "*"
}
}

View file

@ -1,9 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
_G.__ROACT_17_MOCK_SCHEDULER__ = true _G.__ROACT_17_MOCK_SCHEDULER__ = true
local React = require(ReplicatedStorage.Packages.React) local React = require("../../../React")
local ReactRoblox = require(ReplicatedStorage.Packages.ReactRoblox) local ReactRoblox = require("../../../ReactRoblox")
type RenderedHook<Props, Result> = { type RenderedHook<Props, Result> = {
result: Result, result: Result,

View file

@ -1,8 +1,9 @@
local ServerScriptService = game:GetService("ServerScriptService") local suite = require("../../../../test/suite")
local suite = require(ServerScriptService.Tests.suite)
suite.describe("react-charm", function() suite.describe("react-charm", function()
if not game then
return suite.skip()
end
suite.describe("useAtom", function() suite.describe("useAtom", function()
require("./useAtom.spec") require("./useAtom.spec")
end) end)

View file

@ -1,10 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local ReactCharm = require("../")
local renderHook = require("./helpers/renderHook")
local Charm = require(ReplicatedStorage.Packages.Charm) local suite = require("../../../../test/suite")
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
local renderHook = require(script.Parent.helpers.renderHook)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local useAtom = ReactCharm.useAtom local useAtom = ReactCharm.useAtom

View file

@ -1,10 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local ReactCharm = require("../")
local renderHook = require("./helpers/renderHook")
local Charm = require(ReplicatedStorage.Packages.Charm) local suite = require("../../../../test/suite")
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
local renderHook = require(script.Parent.helpers.renderHook)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local useAtomBinding = ReactCharm.useAtomBinding local useAtomBinding = ReactCharm.useAtomBinding

1
packages/react-charm/src/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

View file

@ -1,5 +1,5 @@
local Charm = require(script.Parent.Charm) local Charm = require("./Charm")
local React = require(script.Parent.React) local React = require("./React")
local effect = Charm.effect local effect = Charm.effect
local useBinding = React.useBinding local useBinding = React.useBinding

View file

@ -3,7 +3,14 @@ name = "littensy/react-charm"
version = "0.3.0" version = "0.3.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
exclude = ["tests"] exclude = [
"node_modules",
"src/__tests__",
"src/index.d.ts",
"package.json",
"React.luau",
"Charm.luau",
]
[dependencies] [dependencies]
Charm = "littensy/charm@^0.10" Charm = "littensy/charm@^0.10"

View 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

View file

@ -0,0 +1 @@
return require(script.Parent.Parent.vide.src)

View file

@ -1,5 +1,5 @@
{ {
"name": "VideCharm", "name": "vide-charm",
"tree": { "tree": {
"$path": "src" "$path": "src"
} }

View 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": "*"
}
}

View file

@ -1,8 +1,9 @@
local ServerScriptService = game:GetService("ServerScriptService") local suite = require("../../../../test/suite")
local suite = require(ServerScriptService.Tests.suite)
suite.describe("vide-charm", function() suite.describe("vide-charm", function()
if not game then
return suite.skip()
end
suite.describe("useAtom", function() suite.describe("useAtom", function()
require("./useAtom.spec") require("./useAtom.spec")
end) end)

View file

@ -1,10 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Charm = require("../../Charm")
local ServerScriptService = game:GetService("ServerScriptService") local Vide = require("../../Vide")
local VideCharm = require("../")
local Charm = require(ReplicatedStorage.Packages.Charm) local suite = require("../../../../test/suite")
local Vide = require(ReplicatedStorage.Packages.Vide)
local VideCharm = require(ReplicatedStorage.Packages.VideCharm)
local suite = require(ServerScriptService.Tests.suite)
local atom = Charm.atom local atom = Charm.atom
local root = Vide.root local root = Vide.root

1
packages/vide-charm/src/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

View file

@ -1,5 +1,5 @@
local Charm = require(script.Parent.Charm) local Charm = require("./Charm")
local Vide = require(script.Parent.Vide) local Vide = require("./Vide")
local subscribe = Charm.subscribe local subscribe = Charm.subscribe
local source = Vide.source local source = Vide.source

View file

@ -3,7 +3,14 @@ name = "littensy/vide-charm"
version = "0.3.0" version = "0.3.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
exclude = ["tests"] exclude = [
"node_modules",
"src/__tests__",
"src/index.d.ts",
"package.json",
"Vide.luau",
"Charm.luau",
]
[dependencies] [dependencies]
Charm = "littensy/charm@^0.10" Charm = "littensy/charm@^0.10"

Some files were not shown because too many files have changed in this diff Show more