├── docs ├── intro │ ├── map.md │ ├── derived.md │ ├── ref.md │ ├── get-started.md │ ├── source.md │ ├── operators.md │ └── memory.md ├── internal │ └── conditional-caching.md ├── index.md └── .vitepress │ └── config.mjs ├── selene.toml ├── default.project.json ├── .gitattributes ├── stylua.toml ├── .vscode └── settings.json ├── package.json ├── .gitignore ├── src ├── Memory │ ├── Scoper.luau │ ├── DerivedScope.luau │ └── Kill.luau ├── Internal │ ├── getState.luau │ ├── AddCTask.luau │ ├── ShouldCacheValue.luau │ ├── AddDependent.luau │ └── UpdateAll.luau ├── Creators │ ├── Source.luau │ ├── Ref.luau │ ├── Derived.luau │ └── Map.luau ├── Operators │ ├── Write.luau │ ├── Read.luau │ ├── Batch.luau │ ├── Use.luau │ ├── Track.luau │ ├── InnerScope.luau │ └── Async.luau ├── types.luau ├── Utility │ └── BulkCreate.luau └── init.luau ├── wally.toml ├── dev.project.json ├── aftman.toml ├── tests ├── Operators │ ├── Read.luau │ ├── InnerScope.luau │ ├── Track.luau │ ├── Write.luau │ ├── Use.luau │ └── Async.luau ├── Memory │ ├── Scoper.luau │ ├── DerivedScope.luau │ └── Kill.luau ├── Internal │ ├── AddCTask.luau │ ├── AddDependents.luau │ ├── UpdateAll.luau │ └── ShouldCacheValue.luau └── Creators │ └── Derived.luau ├── benchmarks ├── Creators │ ├── Derived.bench.luau │ └── Source.bench.luau └── internal.bench.luau ├── LICENSE ├── README.md ├── .lune ├── tests.luau └── luneVendor │ └── ecrTestKit.luau ├── CHANGELOG.md └── gh-assets └── Banner.svg /docs/intro/map.md: -------------------------------------------------------------------------------- 1 | # Maps -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /docs/intro/derived.md: -------------------------------------------------------------------------------- 1 | # Deriveds -------------------------------------------------------------------------------- /docs/intro/ref.md: -------------------------------------------------------------------------------- 1 | # References -------------------------------------------------------------------------------- /docs/internal/conditional-caching.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vinum", 3 | "tree" : { 4 | "$path" : "src" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | # Add .luau files to be highlighted as .lua files 4 | *.luau linguist-language=Lua 5 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Tabs" 4 | indent_width = 4 5 | quote_style = "AutoPreferDouble" 6 | collapse_simple_statement = "Never" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.require.mode": "relativeToFile", 3 | "luau-lsp.require.directoryAliases": { 4 | "@lune/": "~/.lune/.typedefs/0.8.0/" 5 | }, 6 | "luau-lsp.sourcemap.rojoProjectFile": "dev.project.json" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.0.0-rc.45" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev docs", 7 | "docs:build": "vitepress build docs", 8 | "docs:preview": "vitepress preview docs" 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project place file 2 | /*.rbxlx 3 | /*.rbxmx 4 | /*.rbxl 5 | /*.rbxm 6 | sourcemap.json 7 | Vunion 8 | build 9 | Packages 10 | node_modules 11 | docs/.vitepress/dist 12 | docs/.vitepress/cache 13 | # Roblox Studio lock files 14 | /*.rbxlx.lock 15 | /*.rbxl.lock -------------------------------------------------------------------------------- /src/Memory/Scoper.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | local function scoper(myTable: T) 6 | return function(): types.Scope 7 | return setmetatable({}, { __index = myTable }) :: any 8 | end 9 | end 10 | 11 | return scoper 12 | -------------------------------------------------------------------------------- /src/Internal/getState.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | local function GetState(x: types.EitherState): types.ReactiveObject 6 | if x.type == "refState" then 7 | return (x :: any).ref 8 | else 9 | return x 10 | end 11 | end 12 | 13 | return GetState 14 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plothan/vinum" 3 | version = "0.5.0" 4 | registry = "https://github.com/UpliftGames/wally-index" 5 | realm = "shared" 6 | description = "A declarative library focusing on making relations between data easy." 7 | 8 | exclude = ["**"] 9 | include = ["src", "src/*", "default.project.json", "wally.lock", "wally.toml"] 10 | [dependencies] -------------------------------------------------------------------------------- /src/Internal/AddCTask.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | type ReactiveObject = types.ReactiveObject 6 | 7 | local function addCTask(RNode: ReactiveObject, CTask: types.CTask) 8 | if not RNode.cTasks then 9 | RNode.cTasks = {} 10 | end 11 | 12 | table.insert(RNode.cTasks :: any, CTask) 13 | end 14 | 15 | return addCTask 16 | -------------------------------------------------------------------------------- /dev.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vinum-dev", 3 | "tree" : { 4 | "$className": "DataModel", 5 | "ReplicatedStorage" : { 6 | "benchmarks" : { 7 | "$path" : "benchmarks" 8 | }, 9 | "Vinum" : { 10 | "$path" : "src" 11 | } 12 | }, 13 | 14 | "ServerScriptService" : { 15 | "$path" : "benchRunner" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | [tools] 6 | rojo = "rojo-rbx/rojo@7.3.0" 7 | wally = "upliftgames/wally@0.3.2" 8 | selene = "kampfkarren/selene@0.25.0" 9 | stylua = "JohnnyMorganz/stylua@0.17.1" 10 | luau-lsp = "JohnnyMorganz/luau-lsp@1.20.2" 11 | lune = "filiptibell/lune@0.8.0" 12 | -------------------------------------------------------------------------------- /src/Creators/Source.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | type ReactiveObject = types.ReactiveObject 6 | 7 | local function Source(scope: types.Scope, value: T): ReactiveObject 8 | local self = { 9 | type = "reactiveState" :: any, 10 | value = value, 11 | scope = scope, 12 | } :: ReactiveObject 13 | 14 | table.insert(scope, self) 15 | return self 16 | end 17 | 18 | return Source 19 | -------------------------------------------------------------------------------- /tests/Operators/Read.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | local Read = Vinum.Read 8 | 9 | TEST("Operators/Read", function() 10 | do 11 | CASE("should read data.") 12 | local scope = Scoped() 13 | 14 | local x = scope:Source(10) 15 | 16 | CHECK(Read(x) == 10) 17 | end 18 | end) 19 | 20 | FINISH() 21 | -------------------------------------------------------------------------------- /src/Operators/Write.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- An operator function that writes a new value to a state struct 3 | -- and launches an update. 4 | 5 | local Package = script.Parent.Parent 6 | local types = require(Package.types) 7 | local Internal = Package.Internal 8 | local UpdateAll = require(Internal.UpdateAll) 9 | 10 | local function Write(object: types.ReactiveObject, newValue: T) 11 | object.value = newValue 12 | UpdateAll(false, false, 1, object) 13 | end 14 | 15 | return Write 16 | -------------------------------------------------------------------------------- /src/Creators/Ref.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | local Internal = Package.Internal 5 | local GetState = require(Internal.GetState) 6 | 7 | local function Ref(scope: types.Scope, state: types.EitherState): types.refState 8 | local myState = GetState(state) 9 | local ref = { 10 | type = "refState", 11 | ref = myState, 12 | } 13 | 14 | table.insert(scope, ref) 15 | return ref :: any 16 | end 17 | 18 | return Ref 19 | -------------------------------------------------------------------------------- /src/Creators/Derived.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | local function Derived(scope: types.Scope, computer: types.Computer, arg: D?): types.ReactiveObject 6 | local self = { 7 | type = "reactiveState" :: any, 8 | _computer = computer, 9 | scope = scope, 10 | _arg = arg, 11 | } 12 | 13 | table.insert(scope, self) 14 | self.value = computer(self, arg or (nil :: any)) 15 | return self 16 | end 17 | 18 | return Derived 19 | -------------------------------------------------------------------------------- /tests/Memory/Scoper.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Scoper = require("../../src/Memory/Scoper") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | TEST("Memory/Scoper", function() 7 | do 8 | CASE("should create a scope creator function") 9 | local flag = false 10 | local scoped = Scoper({ 11 | Add = function(self) 12 | flag = true 13 | end, 14 | }) 15 | 16 | scoped():Add() 17 | CHECK(flag == true) 18 | end 19 | end) 20 | 21 | FINISH() 22 | -------------------------------------------------------------------------------- /tests/Operators/InnerScope.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | 8 | TEST("Operators/InnerScope", function() 9 | do 10 | CASE("should add a CTask with a cleanable field to X") 11 | local scope = Scoped() 12 | local source = scope:Source(10) 13 | 14 | scope:InnerScope(source) 15 | CHECK(((source.cTasks) :: any)[1].cleanable == true) 16 | end 17 | end) 18 | 19 | FINISH() 20 | -------------------------------------------------------------------------------- /src/Operators/Read.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- An operator function that reads the value of either a refState or a state struct. 3 | 4 | local Package = script.Parent.Parent 5 | local types = require(Package.types) 6 | local Internal = Package.Internal 7 | local GetState = require(Internal.GetState) 8 | 9 | local function Read(object: types.EitherState): T 10 | local state = GetState(object) 11 | if state.value == nil then 12 | state.value = (state :: any)._computer(state) 13 | end 14 | local value = state.value 15 | return value 16 | end 17 | 18 | return Read 19 | -------------------------------------------------------------------------------- /src/Memory/DerivedScope.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | local function DerivedScope(myScope: types.Scope, additionalConstructors: U?): types.Scope 6 | local myScopeMetatable = getmetatable(myScope) 7 | local oldConstructors = myScopeMetatable.__index 8 | 9 | if additionalConstructors then 10 | for name, fn in additionalConstructors :: any do 11 | oldConstructors[name] = fn 12 | end 13 | end 14 | return setmetatable({}, { __index = oldConstructors }) :: any 15 | end 16 | 17 | return DerivedScope 18 | -------------------------------------------------------------------------------- /src/Internal/ShouldCacheValue.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | type ReactiveObject = types.ReactiveObject 6 | 7 | local function ShouldCacheValue(RNode: ReactiveObject) 8 | if RNode.dependents then 9 | local areAlliListeners = true 10 | for dependent in RNode.dependents do 11 | if type(dependent) ~= "function" then 12 | areAlliListeners = false 13 | break 14 | end 15 | end 16 | 17 | return not areAlliListeners 18 | else 19 | return true 20 | end 21 | end 22 | 23 | return ShouldCacheValue 24 | -------------------------------------------------------------------------------- /tests/Internal/AddCTask.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local AddCTask = require("../../src/Internal/AddCTask") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local function RNode() 7 | return { 8 | value = 1, 9 | scope = {}, 10 | type = "reactiveState", 11 | } :: any 12 | end 13 | 14 | TEST("Internal/AddCTask", function() 15 | do 16 | CASE("must un-nilify cancelableTasks if its a nil") 17 | local myRNode = RNode() 18 | 19 | AddCTask(myRNode, {} :: any) 20 | CHECK(myRNode.cTasks ~= nil) 21 | end 22 | end) 23 | 24 | FINISH() 25 | -------------------------------------------------------------------------------- /src/Internal/AddDependent.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | type ReactiveObject = types.ReactiveObject 6 | 7 | local function AddDependent(dependency: ReactiveObject, dependent: ReactiveObject | () -> ()) 8 | if not dependency.dependents then 9 | dependency.dependents = {} 10 | end 11 | -- FIXME: Luau doesn't understand guard statements from a typechecking point of 12 | -- view yet, as such, we resort to overriding the types to any. 13 | (dependency :: any).dependents[dependent] = true 14 | end 15 | 16 | return AddDependent 17 | -------------------------------------------------------------------------------- /src/Operators/Batch.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local Internal = Package.Internal 4 | local UpdateAll = require(Internal.UpdateAll) 5 | 6 | local function Batch(...) 7 | local array = { ... } 8 | local arrayLength = #array 9 | local rnodes = {} 10 | local rnodesLength = 0 11 | for i = 1, arrayLength, 2 do 12 | local object = array[i] 13 | local data = array[i + 1] 14 | 15 | table.insert(rnodes, object.node) 16 | rnodesLength += 1 17 | object.value = data 18 | end 19 | 20 | UpdateAll(false, true, rnodesLength, table.unpack(rnodes)) 21 | end 22 | 23 | return Batch 24 | -------------------------------------------------------------------------------- /tests/Operators/Track.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | local Track = Vinum.Track 8 | local Write = Vinum.Write 9 | 10 | TEST("Operators/Track", function() 11 | do 12 | CASE("should run connection when dependency updates") 13 | local scope = Scoped() 14 | 15 | local x = scope:Source(10) 16 | local flag = false 17 | Track(x, function() 18 | flag = true 19 | end) 20 | 21 | Write(x, 20) 22 | CHECK(flag == true) 23 | end 24 | end) 25 | 26 | FINISH() 27 | -------------------------------------------------------------------------------- /tests/Internal/AddDependents.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local AddDependent = require("../../src/Internal/AddDependent") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local function RNode() 7 | return { 8 | value = 1, 9 | scope = {}, 10 | type = "reactiveState", 11 | } :: any 12 | end 13 | 14 | TEST("Internal/AddDependent", function() 15 | do 16 | CASE("must un-nilify dependents if its a nil") 17 | local myRNode = RNode() 18 | local myRNode2 = RNode() 19 | 20 | AddDependent(myRNode, myRNode2) 21 | CHECK(myRNode.dependents ~= nil) 22 | end 23 | end) 24 | 25 | FINISH() 26 | -------------------------------------------------------------------------------- /src/Operators/Use.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- An operator that connects a dependent to a dependency. 3 | 4 | local Package = script.Parent.Parent 5 | local types = require(Package.types) 6 | local Internal = Package.Internal 7 | local GetState = require(Internal.GetState) 8 | local AddDependent = require(Internal.AddDependent) 9 | 10 | local Read = require(script.Parent.Read) 11 | 12 | type ReactiveObject = types.ReactiveObject 13 | local function Use(dependent: ReactiveObject, dependency: types.EitherState): T 14 | local dependencyState = GetState(dependency) 15 | 16 | AddDependent(dependencyState, dependent) 17 | 18 | return Read(dependencyState) 19 | end 20 | 21 | return Use 22 | -------------------------------------------------------------------------------- /benchmarks/Creators/Derived.bench.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local BENCH, START = testkit.benchmark() 5 | local function TITLE(name: string) 6 | print() 7 | print(testkit.color.white(name)) 8 | end 9 | 10 | local Scope = Vinum.Scoped() 11 | 12 | do 13 | TITLE("Derived") 14 | BENCH("Creating Deriveds", function() 15 | for i = 1, START(20_000) do 16 | Scope:Derived(function(RNode) 17 | return 1 18 | end) 19 | end 20 | end) 21 | 22 | BENCH("Updating Deriveds", function() 23 | local v = Scope:Derived(function(RNode) 24 | return 1 25 | end) 26 | 27 | for i = 1, START(20_000) do 28 | (v :: any)._computer(v) 29 | end 30 | end) 31 | end 32 | -------------------------------------------------------------------------------- /tests/Operators/Write.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | local Write = Vinum.Write 8 | local Read = Vinum.Read 9 | 10 | TEST("Operators/Write", function() 11 | do 12 | CASE("should write data to Sources") 13 | local scope = Scoped() 14 | 15 | local x = scope:Source(10) 16 | CHECK(Read(x) == 10) 17 | Write(x, 20) 18 | CHECK(Read(x) == 20) 19 | end 20 | 21 | do 22 | CASE("should write data to Deriveds") 23 | local scope = Scoped() 24 | 25 | local x = scope:Derived(function(self) 26 | return 1 27 | end) 28 | 29 | Write(x, 10) 30 | CHECK(Read(x) == 10) 31 | end 32 | end) 33 | 34 | FINISH() 35 | -------------------------------------------------------------------------------- /docs/intro/get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | Welcome to the Vinum documentation site! With the help of this site, you will learn how to manage state using Vinum, even if you haven't used the library before. 3 | 4 | 5 | ::: warning 6 | 7 | Bear in mind that Vinum is currently still in development, so API stability is not a concern at the moment. 8 | ::: 9 | 10 | ## Installing Vinum 11 | Vinum is distrbuted using [Wally](https://wally.run) under the `plothan` scope, as such, you are required to use Wally to install Vinum. 12 | 13 | ::: info 14 | 15 | When Vinum reaches 1.0.0, it's expected that we will start distrbuting rbxms alongisde wally releases 16 | ::: 17 | 18 | When you finished installing wally, add the following to your `wally.toml`'s `[dependencies]` field: 19 | ```toml 20 | vinum = "plothan/vinum@0.5.0" 21 | ``` -------------------------------------------------------------------------------- /tests/Operators/Use.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | local Use = Vinum.Use 8 | 9 | TEST("Operators/Use", function() 10 | do 11 | CASE("should be able to use States") 12 | local scope = Scoped() 13 | 14 | local x = scope:Source(10) 15 | local y = scope:Source(0) 16 | CHECK(Use(x, y) == 0) 17 | CHECK((y :: any).dependents[x] == true) 18 | end 19 | 20 | do 21 | CASE("should be able to use refStates") 22 | local scope = Scoped() 23 | 24 | local x = scope:Source(10) 25 | local y = scope:Source(0) 26 | CHECK(Use(x, scope:Ref(y)) == 0) 27 | CHECK((y :: any).dependents[x] == true) 28 | end 29 | end) 30 | 31 | FINISH() 32 | -------------------------------------------------------------------------------- /src/Operators/Track.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- A self-aware operator that manages Listen objects. 3 | 4 | local Package = script.Parent.Parent 5 | local types = require(Package.types) 6 | local Internal = Package.Internal 7 | local GetState = require(Internal.GetState) 8 | local AddDependent = require(Internal.AddDependent) 9 | 10 | local function disconnecter(myState: types.ReactiveObject, listener) 11 | return function() 12 | if myState.dependents then 13 | myState.dependents[listener] = nil 14 | end 15 | end 16 | end 17 | 18 | local function Track(state: types.EitherState, listener: () -> ()) 19 | local fn 20 | local myState = GetState(state) 21 | fn = function() 22 | listener() 23 | AddDependent(myState, fn) 24 | end 25 | AddDependent(myState, fn) 26 | 27 | return disconnecter(myState :: any, fn) 28 | end 29 | 30 | return Track 31 | -------------------------------------------------------------------------------- /src/types.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | export type iListener = () -> () 3 | 4 | export type ReactiveObject = { 5 | dependents: { [ReactiveObject | iListener]: boolean }?, 6 | cTasks: { CTask }?, 7 | _computer: ((RNode: ReactiveObject, arg: additionalArg) -> Value)?, 8 | _arg: additionalArg?, 9 | value: Value, 10 | scope: any, 11 | type: "reactiveState", 12 | } 13 | 14 | export type refState = { 15 | type: "refState", 16 | ref: ReactiveObject, 17 | } 18 | 19 | export type EitherState = ReactiveObject | refState 20 | 21 | export type Scope = { [number]: any } & T 22 | 23 | export type Computer = (RNode: ReactiveObject, arg: arg) -> Value 24 | 25 | export type CTask = { 26 | cancel: (self: CTask) -> (), 27 | [any]: any, 28 | } 29 | 30 | return {} 31 | -------------------------------------------------------------------------------- /tests/Internal/UpdateAll.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local AddDependent = require("../../src/Internal/AddDependent") 3 | local UpdateAll = require("../../src/Internal/UpdateAll") 4 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 5 | local TEST, CASE, CHECK, FINISH = testkit.test() 6 | 7 | local function noop() end 8 | local function RNode() 9 | return { 10 | value = 1, 11 | scope = {}, 12 | type = "reactiveState", 13 | } :: any 14 | end 15 | 16 | TEST("Internal/UpdateAll", function() 17 | do 18 | CASE("must update a list of trees") 19 | local root = RNode() 20 | local A = RNode() 21 | local B = noop 22 | 23 | local runs = 0 24 | 25 | A._computer = function(self) 26 | runs += 1 27 | return runs 28 | end 29 | 30 | AddDependent(root, A) 31 | AddDependent(root, B) 32 | 33 | UpdateAll(false, true, 1, root) 34 | 35 | CHECK(runs == 1) 36 | end 37 | end) 38 | 39 | FINISH() 40 | -------------------------------------------------------------------------------- /benchmarks/Creators/Source.bench.luau: -------------------------------------------------------------------------------- 1 | local Vinum = require("../../src") 2 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 3 | local BENCH, START = testkit.benchmark() 4 | local function TITLE(name: string) 5 | print() 6 | print(testkit.color.white(name)) 7 | end 8 | 9 | local Scope = Vinum.Scoped() 10 | do 11 | TITLE("Sources") 12 | BENCH("Creating Sources", function() 13 | for i = 1, START(20_000) do 14 | Scope:Source(i) 15 | end 16 | end) 17 | end 18 | local aSource = function(scope: U, value: T): {nil | T | U | string} 19 | return { 20 | nil, 21 | nil, 22 | nil, 23 | nil, 24 | value, 25 | scope, 26 | "reactiveState", 27 | } 28 | end 29 | 30 | local dScope = Vinum.DerivedScope(Vinum.Scoped(), { 31 | aSource = aSource 32 | }) 33 | do 34 | TITLE("Sources array") 35 | BENCH("Creation", function() 36 | for i = 1, START(20_000) do 37 | dScope:aSource(i) 38 | end 39 | end) 40 | end -------------------------------------------------------------------------------- /src/Operators/InnerScope.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | 3 | local Package = script.Parent.Parent 4 | local types = require(Package.types) 5 | local Memory = Package.Memory 6 | local Internal = Package.Internal 7 | local Kill = require(Memory.Kill) 8 | local DerivedScope = require(Memory.DerivedScope) 9 | local AddCTask = require(Internal.AddCTask) 10 | 11 | type ReactiveObject = types.ReactiveObject 12 | 13 | local function cancel(myScope) 14 | return function(self) 15 | if self.cleanable then 16 | Kill(myScope) 17 | self.cleanable = false 18 | end 19 | end 20 | end 21 | 22 | local function InnerScope(scope: types.Scope, Node: ReactiveObject, additionalConstructors: U?): types.Scope 23 | local myScope = DerivedScope(scope, additionalConstructors) 24 | 25 | AddCTask(Node, { 26 | cancel = cancel(myScope), 27 | cleanable = true, 28 | }) 29 | 30 | return myScope 31 | end 32 | 33 | return InnerScope 34 | -------------------------------------------------------------------------------- /src/Creators/Map.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local Internal = Package.Internal 4 | local GetState = require(Internal.GetState) 5 | local Creators = Package.Creators 6 | local Derived = require(Creators.Derived) 7 | local Operators = Package.Operators 8 | local Use = require(Operators.Use) 9 | local types = require(Package.types) 10 | 11 | type ReactiveObject = types.ReactiveObject 12 | 13 | local function Map( 14 | scope: types.Scope, 15 | myState: types.EitherState, 16 | cases: { [U]: T | types.EitherState } 17 | ): ReactiveObject 18 | return Derived(scope, function(Node, args) 19 | local myValue = Use(Node, args.myState) 20 | local chosenArm = args.cases[myValue] or cases["default" :: any] 21 | 22 | if type(chosenArm) == "table" and chosenArm.type then 23 | return Use(Node, GetState(chosenArm)) 24 | else 25 | return chosenArm :: T 26 | end 27 | end, { cases = cases, myState = GetState(myState) }) 28 | end 29 | 30 | return Map 31 | -------------------------------------------------------------------------------- /src/Utility/BulkCreate.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | 5 | local Derived = require(Package.Creators.Derived) 6 | local Source = require(Package.Creators.Source) 7 | local Ref = require(Package.Creators.Ref) 8 | local Map = require(Package.Creators.Map) 9 | 10 | local scoper = require(Package.Memory.Scoper) 11 | local InnerScope = require(Package.Operators.InnerScope) 12 | 13 | local function Add(scope, thing) 14 | table.insert(scope, thing) 15 | 16 | return thing 17 | end 18 | local scoped = scoper({ 19 | Derived = Derived, 20 | Source = Source, 21 | Ref = Ref, 22 | Map = Map, 23 | InnerScope = InnerScope, 24 | Add = Add, 25 | }) 26 | 27 | type Scope = typeof(scoped()) 28 | 29 | local function BulkCreate(fn: (T & { [any]: types.ReactiveObject }, scope: Scope) -> ()): (T, Scope) 30 | local results = {} 31 | local scope = scoped() 32 | 33 | fn(results :: any, scope) 34 | 35 | return results :: any, scope 36 | end 37 | 38 | return BulkCreate 39 | -------------------------------------------------------------------------------- /tests/Memory/DerivedScope.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Scoper = require("../../src/Memory/Scoper") 3 | local DerivedScope = require("../../src/Memory/DerivedScope") 4 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 5 | local TEST, CASE, CHECK, FINISH = testkit.test() 6 | 7 | TEST("Memory/DerivedScope", function() 8 | do 9 | CASE("should inherit the constructors of the scope") 10 | local flag = false 11 | local myScope = Scoper({ 12 | add = function(self) 13 | flag = true 14 | end, 15 | })() 16 | local dScope = DerivedScope(myScope, { 17 | hi = function(self) 18 | flag = false 19 | end, 20 | }) 21 | 22 | dScope:add() 23 | CHECK(flag == true) 24 | dScope:hi() 25 | CHECK(flag == false) 26 | end 27 | 28 | do 29 | CASE("should merge additional constructors") 30 | local myScope = Scoper({ 31 | add = function(self) 32 | return true 33 | end, 34 | })() 35 | local derivedScope = DerivedScope(myScope, { 36 | subtract = function(self) 37 | return false 38 | end, 39 | }) 40 | 41 | CHECK(derivedScope:subtract() == false) 42 | end 43 | end) 44 | 45 | FINISH() 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sinlernick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Internal/ShouldCacheValue.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local AddDependent = require("../../src/Internal/AddDependent") 3 | local ShouldCacheValue = require("../../src/Internal/ShouldCacheValue") 4 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 5 | local TEST, CASE, CHECK, FINISH = testkit.test() 6 | 7 | local function RNode() 8 | return { 9 | value = 1, 10 | scope = {}, 11 | type = "reactiveState", 12 | } :: any 13 | end 14 | 15 | TEST("Internal/ShouldCacheValue", function() 16 | do 17 | CASE("should return true if it has no dependents ") 18 | local root = RNode() 19 | 20 | CHECK(ShouldCacheValue(root) == true) 21 | end 22 | 23 | do 24 | CASE("should return false if it has only iListeners") 25 | local root = RNode() 26 | 27 | AddDependent(root, function() end) 28 | CHECK(ShouldCacheValue(root) == false) 29 | end 30 | 31 | do 32 | CASE("should return true if it finds a different type of dependent") 33 | local root = RNode() 34 | local a = RNode() 35 | 36 | AddDependent(root, a) 37 | AddDependent(root, function() end) 38 | CHECK(ShouldCacheValue(root) == true) 39 | end 40 | end) 41 | 42 | FINISH() 43 | -------------------------------------------------------------------------------- /benchmarks/internal.bench.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local UpdateAll = require("../src/Internal/UpdateAll") 3 | local AddDependents = require("../src/Internal/AddDependent") 4 | 5 | local testkit = require("../.lune/luneVendor/ecrTestKit") 6 | local BENCH, START = testkit.benchmark() 7 | local function TITLE(name: string) 8 | print() 9 | print(testkit.color.white(name)) 10 | end 11 | 12 | local function RNode() 13 | return { 14 | value = 1, 15 | type = "reactiveState", 16 | } :: any 17 | end 18 | 19 | do 20 | TITLE("UpdateAll") 21 | do 22 | local a = RNode() 23 | local b = RNode() 24 | local root = RNode() 25 | 26 | AddDependents(root, a) 27 | AddDependents(root, b) 28 | 29 | BENCH("with direct dependents", function() 30 | for i = 1, START(20_000) do 31 | UpdateAll(true, true, 1, root) 32 | end 33 | end) 34 | end 35 | 36 | local a = RNode() 37 | local b = RNode() 38 | local root = RNode() 39 | 40 | AddDependents(root, a) 41 | AddDependents(root, b) 42 | AddDependents(a, RNode()) 43 | BENCH("with nested dependents", function() 44 | for i = 1, START(20_000) do 45 | UpdateAll(true, true, 1, root) 46 | end 47 | end) 48 | end 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # What is Vinum? 4 | 5 | Vinum is a declarative library that focuses on making state management both truly reactive and scalable. 6 | 7 | ## Features 8 | 9 | ### :zap: Performance Oriented 10 | 11 | Vinum is designed with performance in mind, and it's API is oriented towards performance, so you will naturally find yourself writing efficient code with it. 12 | 13 | ### :lock: Type Safety 14 | Vinum is completely written with Luau's strict type system in mind, so you can get full autocomplete and linting in your editor. Additionally, this means that Luau will be able to catch some errors before running which can decrease development time. 15 | 16 | 17 | ### :bangbang: Built-in Memory Management 18 | 19 | Vinum forces standard and *useful* manual memory tools to achieve predictability and eliminate most (if not all!) edge cases. 20 | 21 | ### :chains: Dependencies Supercharged 22 | 23 | Vinum manages dependencies for you in a very optimized and stable way so that bugs that relate to dependencies are often non-existent. 24 | 25 | ## Contributing 26 | If you ever want to contribute for the Vinum project, please refer to [this](https://github.com/vinum-team/Vinum/blob/master/CONTRIBUTING.MD) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Vinum" 7 | text: "A declarative and speedy state management library." 8 | tagline: With a lightweight, fast and declarative approach, state management have never been this easy. 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /intro/get-started 13 | 14 | features: 15 | - title: Performance 16 | icon: ⚡ 17 | details: Vinum is designed with performance in mind, and it’s API is oriented towards performance, so you will naturally find yourself writing efficient code with it. 18 | - title: Type Safety 19 | icon: 🔒 20 | details: Vinum is completely written with Luau’s strict type system in mind, so you can get full autocomplete and linting in your editor. Additionally, this means that Luau will be able to catch some errors before running which can decrease development time. 21 | - title: Declarative 22 | icon: 📃 23 | details: Instead of performing user-level operations to sync between data, relationships are declared. 24 | - title: Batteries-packed 25 | icon: 🔋 26 | details: From memory management utilities, to async task managements, Vinum have got you covered. 27 | --- 28 | 29 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: "Vinum", 8 | description: "The website for the Vinum Project.", 9 | themeConfig: { 10 | // https://vitepress.dev/reference/default-theme-config 11 | nav: [ 12 | { text: 'Home', link: '/' }, 13 | ], 14 | sidebar: [ 15 | { 16 | text: "Introduction", 17 | items: [ 18 | {text: "Understanding Memory in Vinum", link: "/intro/memory"}, 19 | {text: "Storing Simple Data", link: "/intro/source"}, 20 | {text: "Operating On Data", link: "/intro/operators"}, 21 | {text: "Deriving New Data", link: "/intro/derived"}, 22 | {text: "Mapping Data to Another", link: "/intro/map"}, 23 | {text: "Referencing Data Sources", link: "/intro/ref"}, 24 | ] 25 | }, 26 | { 27 | text: "Internal", 28 | items: [ 29 | {text: "Conditional Value Caching", link: "/internal/conditional-caching"}, 30 | ] 31 | }, 32 | ], 33 | 34 | socialLinks: [ 35 | { icon: 'github', link: 'https://github.com/vinum-team/Vinum' } 36 | ] 37 | }, 38 | markdown: { 39 | lineNumbers: true 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /tests/Operators/Async.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local task = require("@lune/task") 3 | local Vinum = require("../../src") 4 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 5 | local TEST, CASE, CHECK, FINISH = testkit.test() 6 | 7 | local Scoped = Vinum.Scoped 8 | local Async = Vinum.Async 9 | 10 | TEST("Operators/Async", function() 11 | do 12 | CASE("should manage the async operation") 13 | local scope = Scoped() 14 | 15 | local ran = 0 16 | scope:Derived(function(self) 17 | Async(self, function() 18 | task.wait(2) 19 | ran += 1 20 | end) 21 | 22 | return 0 23 | end) 24 | CHECK(ran == 0) 25 | task.wait(2) 26 | CHECK(ran == 1) 27 | end 28 | 29 | do 30 | CASE("should cancel async operations") 31 | 32 | local scope = Scoped() 33 | 34 | local x = scope:Source(10) 35 | local first_flag = false 36 | local second_flag = false 37 | scope:Derived(function(self) 38 | Async(self, function() 39 | first_flag = not first_flag 40 | task.wait(1) 41 | second_flag = not second_flag 42 | end) 43 | return Vinum.Use(self, x) 44 | end) 45 | CHECK(first_flag == true) 46 | CHECK(second_flag == false) 47 | Vinum.Write(x, 20) 48 | CHECK(first_flag == false) 49 | CHECK(second_flag == false) 50 | 51 | task.wait(1) 52 | CHECK(second_flag == true) 53 | end 54 | end) 55 | 56 | FINISH() 57 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Derived = require(script.Creators.Derived) 3 | local Source = require(script.Creators.Source) 4 | local Ref = require(script.Creators.Ref) 5 | local Map = require(script.Creators.Map) 6 | 7 | local scoper = require(script.Memory.Scoper) 8 | local InnerScope = require(script.Operators.InnerScope) 9 | 10 | local function Add(scope, thing) 11 | table.insert(scope, thing) 12 | 13 | return thing 14 | end 15 | local scoped = scoper({ 16 | Derived = Derived, 17 | Source = Source, 18 | Ref = Ref, 19 | Map = Map, 20 | InnerScope = InnerScope, 21 | Add = Add, 22 | }) 23 | 24 | local Track = require(script.Operators.Track) 25 | local Use = require(script.Operators.Use) 26 | local Read = require(script.Operators.Read) 27 | local Write = require(script.Operators.Write) 28 | local Batch = require(script.Operators.Batch) 29 | local Async = require(script.Operators.Async) 30 | 31 | local DerivedScope = require(script.Memory.DerivedScope) 32 | local Kill = require(script.Memory.Kill) 33 | 34 | local BulkCreate = require(script.Utility.BulkCreate) 35 | return { 36 | Scoped = scoped, 37 | Track = Track, 38 | Use = Use, 39 | Read = Read, 40 | Write = Write, 41 | Kill = Kill, 42 | Batch = Batch, 43 | Async = Async, 44 | DerivedScope = DerivedScope, 45 | BulkCreate = BulkCreate, 46 | Version = { 47 | major = 0, 48 | minor = 5, 49 | patch = 0, 50 | dev_mode = true, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /tests/Memory/Kill.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local AddCTask = require("../../src/Internal/AddCTask") 4 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 5 | local TEST, CASE, CHECK, FINISH = testkit.test() 6 | 7 | local Scoped = Vinum.Scoped 8 | local Kill = Vinum.Kill 9 | local Use = Vinum.Use 10 | 11 | TEST("Memory/Kill", function() 12 | do 13 | CASE("should run any functions stored in the scope") 14 | local scope = Scoped() 15 | 16 | local flag = false 17 | scope:Add(function() 18 | flag = true 19 | end) 20 | 21 | CHECK(flag == false) 22 | Kill(scope) 23 | CHECK(flag == true) 24 | end 25 | 26 | do 27 | CASE("should cleanup the value and CTasks of the state struct") 28 | local scope = Scoped() 29 | 30 | local x = scope:Source(10) 31 | local flag = false 32 | AddCTask(x, { 33 | cancel = function(self) 34 | flag = true 35 | end, 36 | }) 37 | 38 | Kill(scope) 39 | CHECK(x.value == nil) 40 | CHECK(x.cTasks == nil) 41 | CHECK(flag == true) 42 | end 43 | do 44 | CASE("should cleanup the scope of dependent state structs") 45 | local firstScope = Scoped() 46 | local secondScope = Scoped() 47 | 48 | local x = firstScope:Source(10) 49 | local y = secondScope:Derived(function(Node) 50 | return Use(Node, x) 51 | end) 52 | 53 | Kill(firstScope) 54 | CHECK(y.value == nil) 55 | end 56 | end) 57 | 58 | FINISH() 59 | -------------------------------------------------------------------------------- /src/Operators/Async.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local Internal = Package.Internal 4 | local AddCTask = require(Internal.AddCTask) 5 | local types = require(Package.types) 6 | 7 | type ReactiveObject = types.ReactiveObject 8 | 9 | local coroutine_pool: { [number]: thread } = {} 10 | 11 | local function yielder() 12 | while true do 13 | local callback, thread = coroutine.yield() 14 | callback() 15 | table.insert(coroutine_pool, thread) 16 | end 17 | end 18 | 19 | -- get_free_coroutine returns a coroutine owned by the caller, meaning that until 20 | -- the caller actually uses this coroutine using run_coroutine, the pool won't be 21 | -- able to retrieve it. 22 | local function get_free_coroutine() 23 | local free_coroutine = coroutine_pool[#coroutine_pool] 24 | if free_coroutine then 25 | coroutine_pool[#coroutine_pool] = nil 26 | return free_coroutine 27 | end 28 | 29 | free_coroutine = coroutine.create(yielder) 30 | coroutine.resume(free_coroutine :: any) 31 | 32 | return free_coroutine :: any 33 | end 34 | 35 | local function run_coroutine(my_coroutine, callback, argument: any) 36 | coroutine.resume(my_coroutine, callback, my_coroutine, argument) 37 | end 38 | 39 | function cancel(self) 40 | if coroutine.status(self.co) == "dead" then 41 | return 42 | end 43 | coroutine.close(self.co) 44 | end 45 | 46 | local function Async(self: ReactiveObject, fn: () -> ()) 47 | local co = get_free_coroutine() 48 | 49 | local CTask = { 50 | cancel = cancel, 51 | co = co, 52 | } 53 | 54 | AddCTask(self, CTask :: any) 55 | run_coroutine(co, fn) 56 | return CTask 57 | end 58 | 59 | return Async 60 | -------------------------------------------------------------------------------- /tests/Creators/Derived.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Vinum = require("../../src") 3 | local testkit = require("../../.lune/luneVendor/ecrTestKit") 4 | local TEST, CASE, CHECK, FINISH = testkit.test() 5 | 6 | local Scoped = Vinum.Scoped 7 | local Use = Vinum.Use 8 | local Write = Vinum.Write 9 | local Read = Vinum.Read 10 | local Track = Vinum.Track 11 | 12 | TEST("Creators/Derived", function() 13 | print("yes") 14 | do 15 | CASE("should recompute everytime a dependency updates") 16 | local scope = Scoped() 17 | local x = scope:Source(10) 18 | -- y = 2x 19 | local y = scope:Derived(function(RNode) 20 | return Use(RNode, x) * 2 21 | end) 22 | 23 | Write(x, 20) 24 | CHECK(Read(y) == 40) 25 | end 26 | do 27 | CASE("should release value and its cleanable CTasks when possible") 28 | local scope = Scoped() 29 | local runs = 0 30 | local x = scope:Source(10) 31 | local Derived = scope:Derived(function(RNode) 32 | scope:InnerScope(RNode):Add(function() 33 | runs += 1 34 | end) 35 | return Use(RNode, x) 36 | end) 37 | Track(Derived, function() end) 38 | 39 | Write(x, 0) 40 | -- NOTE: It's two runs since after Write, all of CTasks are cancelled to 41 | -- prepare for a new update (state recomputation), which counts as the first 42 | -- time, and then later down in the pipeline, CTasks that have the cleanable flag 43 | -- get cancelled as well. 44 | CHECK(runs == 2) 45 | end 46 | 47 | do 48 | CASE("optional and additional args should work") 49 | local scope = Scoped() 50 | local out 51 | 52 | scope:Derived(function(RNode, argument) 53 | out = argument 54 | return 1 55 | end, 100) 56 | 57 | CHECK(out == 100) 58 | end 59 | end) 60 | 61 | FINISH() 62 | -------------------------------------------------------------------------------- /docs/intro/source.md: -------------------------------------------------------------------------------- 1 | # Storing Simple Data 2 | The simplest way to store state in Vinum is through `Sources`, which are simple state containers with no special behavior: 3 | ```lua 4 | local myHealth = scope:Source(100) 5 | ``` 6 | 7 | ::: warning Operator-Safe 8 | `Source` is a table that adheres to ReactiveObjects, meaning it can be used with Vinum's Supplied [Operators](./operators). 9 | ::: 10 | 11 | ::: danger Unsafe Public Indexing 12 | `ReactiveObject`'s structure isn't stablized between versions, as such you aren't recommended to directly index its fields directly. 13 | ::: 14 | 15 | The sole reason for Source's existence is the vital need for a basic container that ***just*** stores whatever data you input into it. As such, Sources are considered one of the vital building blocks of any Vinum code. 16 | 17 | ### ReactiveObjects: Why and How 18 | 19 | Consider the following code: 20 | ```lua 21 | local coins = 100 22 | local doubleCoins = coins * 2 23 | ``` 24 | 25 | In this example, doubleCoins are set to the value that is half coins' value, but only for ***once***. This is due to the fact that Luau variables aren't capable of storing other values that use them in their computations, or in simpler words, don't store their dependents. As such, the responsibility of keeping your state synced is in your own hands, which turns out to be a bad developer experience because no matter the approach, you will have to manually store a list of dependents. 26 | 27 | As such, ReactiveObjects were born to solve the issue of reactivity in luau. They are simple objects that store a `type` and`value` fields alongside some internal values. 28 | 29 | As a user, you wouldn't create them directly and instead use something like `Source`s. Additonally, they don't store methods and instead are operated on using [Operators](./operators). -------------------------------------------------------------------------------- /src/Memory/Kill.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- A builtin function responsible for killing scopes. 3 | local Package = script.Parent.Parent 4 | local Internal = Package.Internal 5 | local GetState = require(Internal.GetState) 6 | local types = require(Package.types) 7 | 8 | type ReactiveObject = types.ReactiveObject 9 | 10 | local function cancelAllCTasks(RNode: ReactiveObject) 11 | if RNode.cTasks then 12 | for _, ctask in RNode.cTasks do 13 | ctask:cancel() 14 | end 15 | 16 | RNode.cTasks = nil 17 | end 18 | end 19 | 20 | local function doCleanup(task) 21 | local taskType = typeof(task) 22 | if taskType == "Instance" then 23 | task:Destroy() 24 | elseif taskType == "RBXScriptConnection" then 25 | task:Disconnect() 26 | elseif taskType == "function" then 27 | (task :: any)() 28 | elseif taskType == "table" then 29 | if typeof(task.destroy) == "function" then 30 | task:destroy() 31 | elseif typeof(task.Destroy) == "function" then 32 | task:Destroy() 33 | elseif task[1] ~= nil then 34 | for _, subtask in task do 35 | doCleanup(subtask) 36 | end 37 | end 38 | end 39 | end 40 | 41 | local function Kill(scope: types.Scope) 42 | for _, value in scope :: any do 43 | -- cases 44 | -- case 1: Vinum Structs 45 | local valueType = type(value) 46 | 47 | if valueType == "table" and value.type then 48 | local state = GetState(value) 49 | cancelAllCTasks(state) 50 | state.value = nil 51 | 52 | if not state.dependents then 53 | continue 54 | end 55 | 56 | for child: types.ReactiveObject in (state :: any).dependents do 57 | if type(child) == "function" then 58 | continue 59 | end 60 | if child.scope ~= scope then 61 | Kill(child.scope) 62 | end 63 | end 64 | continue 65 | end 66 | 67 | doCleanup(value) 68 | end 69 | 70 | table.clear(scope) 71 | end 72 | 73 | return Kill 74 | -------------------------------------------------------------------------------- /src/Internal/UpdateAll.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | local Package = script.Parent.Parent 3 | local types = require(Package.types) 4 | local ShouldCacheValue = require(script.Parent.ShouldCacheValue) 5 | 6 | type ReactiveObject = types.ReactiveObject 7 | 8 | local function UpdateAll( 9 | disableDependentClearing: boolean, 10 | updateRoot: boolean, 11 | numberOfRoots: number, 12 | ...: ReactiveObject 13 | ) 14 | local queue: { ReactiveObject | () -> () } = { ... } 15 | local length = numberOfRoots 16 | local pos = 1 17 | local lastOccurances = {} 18 | 19 | local rootLength = length 20 | 21 | while pos <= length do 22 | local currentObject = queue[pos] 23 | lastOccurances[currentObject] = pos 24 | if type(currentObject) == "table" and currentObject.dependents then 25 | for dependent in currentObject.dependents do 26 | length += 1 27 | queue[length] = dependent 28 | end 29 | end 30 | 31 | pos += 1 32 | end 33 | 34 | local toConditioanllyClean = {} 35 | 36 | for index, object in queue do 37 | if index <= rootLength and not updateRoot then 38 | continue 39 | end 40 | if lastOccurances[object :: any] ~= index then 41 | continue 42 | end 43 | 44 | if type(object) == "function" then 45 | object() 46 | else 47 | if object.cTasks then 48 | for _, ctask in object.cTasks do 49 | ctask:cancel() 50 | end 51 | end 52 | 53 | if not disableDependentClearing then 54 | object.dependents = nil 55 | end 56 | if object._computer then 57 | object.value = object._computer(object, object._arg) 58 | table.insert(toConditioanllyClean, object) 59 | end 60 | end 61 | end 62 | 63 | for _, object in toConditioanllyClean do 64 | if ShouldCacheValue(object) then 65 | continue 66 | end 67 | 68 | object.value = nil 69 | 70 | if object.cTasks then 71 | for _, cTask in object.cTasks do 72 | if cTask.cleanable then 73 | cTask:cancel() 74 | end 75 | end 76 | end 77 | end 78 | return queue, lastOccurances 79 | end 80 | 81 | return UpdateAll 82 | -------------------------------------------------------------------------------- /.lune/tests.luau: -------------------------------------------------------------------------------- 1 | local fs = require("@lune/fs") 2 | local scriptString = [[ 3 | local function PathHolder(currentPath) 4 | return setmetatable({ path = currentPath }, { 5 | __index = function(self, index) 6 | if index then 7 | if index == "Parent" then 8 | return PathHolder(`{currentPath}../}`) 9 | else 10 | return PathHolder(`{string.gsub(currentPath, "}", "")}{index}/`) 11 | end 12 | else 13 | return self 14 | end 15 | end, 16 | }) 17 | end 18 | 19 | local script = setmetatable({ path = "" }, { 20 | __index = function(self, index) 21 | if index == "" then 22 | return script 23 | end 24 | 25 | if index ~= "Parent" then 26 | return PathHolder(`{index}/`) 27 | end 28 | 29 | if index == "Parent" then 30 | return PathHolder("") 31 | end 32 | 33 | return script 34 | end, 35 | }) 36 | ]] 37 | 38 | local requireScript = [[ 39 | local function requireImpl(thing) 40 | if type(thing) == "string" then 41 | return require(string.gsub(thing, "src", "procSrc")) 42 | end 43 | return require(thing.path) 44 | end 45 | ]] 46 | 47 | 48 | local function transformSrc(startingPath: string) 49 | for _, name in fs.readDir(startingPath) do 50 | local path = `{startingPath}/{name}` 51 | if fs.isDir(path) then 52 | transformSrc(path) 53 | else 54 | local content = fs.readFile(path) 55 | fs.writeFile( 56 | path, 57 | `{requireScript} \n {scriptString} \n return (function() \n {string.gsub(content, "require", "requireImpl")} end)()` 58 | ) 59 | end 60 | end 61 | end 62 | 63 | --[[ 64 | Wrap all source files so that we can inject proxy requires and 65 | `script`s 66 | ]] 67 | fs.copy("src", "procSrc") 68 | transformSrc("procSrc") 69 | 70 | --[[ 71 | Wrap all test files so that we can inject proxy requires 72 | ]] 73 | 74 | fs.copy("tests", "procTests") 75 | transformSrc("procTests") 76 | 77 | local function run(path) 78 | for _, itemName in fs.readDir(path) do 79 | local itemPath = `{path}/{itemName}` 80 | if fs.isFile(itemPath) and itemName:find(".luau") then 81 | require(`../` .. itemPath) 82 | elseif fs.isDir(itemPath) then 83 | run(itemPath) 84 | end 85 | end 86 | end 87 | 88 | fs.copy("benchmarks", "proc-bench") 89 | transformSrc("proc-bench") 90 | run("proc-bench") 91 | run("procTests") 92 | 93 | fs.removeDir("proc-bench") 94 | fs.removeDir("procSrc") 95 | fs.removeDir("procTests") -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelogs 2 | 3 | Any changes that hold a value should be listed here under the Version that they will be released under. 4 | ## Unreleased 5 | ### Added 6 | * Added the ability for Deriveds to cache optional arguments supplied by the user. This helps with cases where building more complex state objects on top of Deriveds (such as Maps) requires caching some essential data to help Luau better optimise our code. 7 | * Re-added `Vinum.Version` table. 8 | * `Track` now returns an `untrack` function used for disconnecting functions. 9 | * Added `Async` operator responsible for spawning async operations. 10 | * Added `UseMiddleware` to register middleware for operations such as `read`ing, `write`ing. 11 | ### Changed 12 | * Removed RNode and related types and instead implemented a generic `ReactiveObject` type that all state objects adhere to. This lowers the memory usage and generally decreases the processing time. 13 | * Simplified iListeners to simple functions. 14 | * Renamed `On` operator to `Track`. 15 | * `Write` can now write to Deriveds and similar state objects. 16 | ### Removed 17 | * Removed Selects (and additionally, the hardcoded support for it in all other places), in preparation for new tools to help with state centralization. 18 | 19 | ## V0.4.2 20 | * Exposes the `DerivedScope`. 21 | ## V0.4.1 22 | * Fixed a bug in Select where it would not init its value until the next update. 23 | * Some internal changes in Derived.luau and Select.luau. 24 | ## V0.4 25 | * Changes are untracked until 1.0.0. 26 | ## V0.3 27 | * Everything is completely refined, and changed since the last version. Everything below is just a "summary" of the new major features. 28 | * Async Operations are now supported. 29 | * Mirror, Reflect, Match and other similar objects are now moved into a single object. 30 | * Observe is now changed to listener, and now has the ability to remember old state. 31 | * Ability to cancel dependency tree recalcuation is now present. 32 | * Groups are now removed, and now a `KeyOf` object is present to caputre a "selection" of a state object that holds a table. 33 | * A new dependency tree simplifier. 34 | ## V0.2 35 | * `Vinum.JustOk` was renamed to `Vinum.AlwaysTrue` for a more descriptive API for begineers 36 | * `Observe:onBind`, which fires the provided function, and then returns a connection disconnector. 37 | * `Vinum.Mirror` object, which is used for directly mirroring Objects' values, as opposed to `Vinum.Match`, which perform a computation instead. 38 | * `Vinum.Reflect` object, which is used directly to mirror a specific object's value. Useful for creating readonly clones of a state object. 39 | * `Vinum.Wrap` object, which is used to derive state objects from a luau-pure signal/RBXScriptSignal. 40 | * A fix for Match, which is to fix an error that can occur when working with self-contained `Vinum.Group` keys. 41 | * `Vinum.Record` object, which is used to store "records" of a specific object's value. 42 | * `Vinum.Version` table, which is used to identify the version that this module is built on 43 | * `Vinum.Destroy` function, which enables you to destroy state objects. 44 | ## V0.1 45 | Initial Release. -------------------------------------------------------------------------------- /docs/intro/operators.md: -------------------------------------------------------------------------------- 1 | # Operating on Data 2 | Vinum provides many operator that help you perform many operations, from something as simple as reading, to something complex as managing async tasks. 3 | ::: details Performance Details 4 | 5 | Some of the following Operators are inlineable by Luau's level two Complier optimizations, which means operation calls are fast. 6 | ::: 7 | 8 | ## Reading using `Read` 9 | `Read` is an `inlineable` Operator that retreives the value of a `ReactiveObject` or a `refState` (created by [Refs](/intro/ref)). 10 | ```lua 11 | local Read = Vinum.Read 12 | local scope = Vinum.Scoped() 13 | 14 | local x = scope:Source(100) 15 | 16 | print(Read(x)) -- prints '100' 17 | ``` 18 | 19 | ::: warning Performance Reliant on [Conditional Value Caching](/internal/conditional-caching) 20 | ::: 21 | 22 | ## Writing using `Write` 23 | `Write` is an `inlineable` Operator the sets the value of a `ReactiveObject`. 24 | ```lua 25 | local Read = Vinum.Read 26 | local Write = Vinum.Write 27 | local scope = Vinum.Scoped() 28 | 29 | local x = scope:Source(100) 30 | Write(x, 200) 31 | 32 | print(Read(x)) -- prints '200' 33 | ``` 34 | ## Tracking Changes using `Track` 35 | `Track` is an `inlineable` Operator that allows you to run callbacks when a `ReactiveObject`/`refState` updates. 36 | ```lua 37 | local Read = Vinum.Read 38 | local Write = Vinum.Write 39 | local Track = Vinum.Track 40 | local scope = Vinum.Scoped() 41 | 42 | local x = scope:Source(100) 43 | 44 | Track(x, function() 45 | print("Hi, x was changed!") 46 | end) 47 | 48 | Write(x, 200) 49 | 50 | -- "Hi, x was changed!" was printed. 51 | ``` 52 | 53 | Additionally, it returns an `untrack` function that allows you to disable the tracking: 54 | ```lua{12} 55 | local Read = Vinum.Read 56 | local Write = Vinum.Write 57 | local Track = Vinum.Track 58 | local scope = Vinum.Scoped() 59 | 60 | local x = scope:Source(100) 61 | 62 | local untrack = Track(x, function() 63 | print("Hi, x was changed!") 64 | end) 65 | 66 | untrack() 67 | 68 | Write(x, 200) 69 | 70 | -- nothing was printed. 71 | ``` 72 | ## Using Dependencies using `Use` 73 | `Use` is an `inlineable` Operator that adds a `ReactiveObject` as an dependent to another `ReactiveObject`/`refState`. More commonly used within `Deriveds`: 74 | ```lua 75 | local Read = Vinum.Read 76 | local Write = Vinum.Write 77 | local Use = Vinum.Use 78 | local scope = Vinum.Scoped() 79 | 80 | local x = scope:Source(100) 81 | local y = scope:Derived(function(self) 82 | return Use(self, x) / 2 83 | end) 84 | 85 | Write(x, 200) 86 | print(Read(y)) -- "100" is printed. 87 | ``` 88 | ::: warning Performance Reliant on [Conditional Value Caching](/internal/conditional-caching) 89 | ::: 90 | ::: danger `Use` dependencies aren't persistent! 91 | 92 | Vinum automatically recaptures all dependents in every update, as such, dependencies aren't stable and need to be re-added every update. Due to this, you are advised to use `Use` only in `Derived` callbacks. 93 | ::: 94 | 95 | ## Managing Memory For ReactiveObjects using `InnerScope` 96 | ## Batching Write Requests using `Batch` 97 | ## Running ReactiveObject-dependent Async Tasks using `Async` -------------------------------------------------------------------------------- /docs/intro/memory.md: -------------------------------------------------------------------------------- 1 | # Understanding Memory in Vinum 2 | Memory is such an important concept in Vinum specifically because the way the API is designed. By design, a certain approach to memory management is enforced when working with Vinum, as such, learning how it works is vital to you as a user. 3 | 4 | ## The Problem 5 | Values in Luau are of two very different types: 6 | * Garbage-collection-safe (gc-safe) data types, such as numbers, bools, and tables containing other gc-safe types, etc. 7 | * Manual Managed data types, such as Roblox Instances, OOP Objects, etc. 8 | 9 | The first type is very easy to work with and let Luau do the work for you as long as you carefully don't reference it forever. 10 | 11 | It's when you begin working with the manually managed data types that memory management become annoying because as the developer you should always pair a constructor call with a deconstructor call. That is, you can't `Destroy()` an Instance twice, but you must also call it one time. 12 | 13 | As it turns out, this way isn't exactly great for developer experience and developers usually don't do this right. 14 | 15 | ## The Solution: Vinum Scopes 16 | 17 | In Vinum, Scopes are a magical type where you can `table.insert()` into them stuff for cleanup, and also call constructors as methods from them. For example: 18 | ```lua 19 | local Health = scope:Source(100) 20 | 21 | table.insert(scope, Instance.new("Part", workspace)) 22 | ``` 23 | ::: info 24 | 25 | Fun fact: Vinum constructors also technically call `table.insert()` as well internally, which means the only way to add something to a scope is to insert something to it 26 | ::: 27 | 28 | Here, both Health and our Part will be cleaned up when our scope is destroyed. 29 | 30 | The way they are designed is to assist luau in enforcing the usage of scopes, rather than making them an opt-in paradigm which means when you forget to use them, Luau will warn you ahead of time. 31 | 32 | ### Creating A Scope 33 | 34 | To create a scope, first import the `Scoped` function, and then call it: 35 | ```lua 36 | local Scoped = Vinum.Scoped 37 | 38 | local scope = Scoped() 39 | ``` 40 | 41 | By default, `Scoped` returns a list of the constructors for Vinum objects (Maps, Sources, etc..) and an `Add` method that can be used for adding anything: 42 | ```lua 43 | local mySource = scope:Source(...) 44 | local myMap = scope:Map(...) 45 | 46 | local myFn = scope:Add(function() 47 | print("hi") 48 | end) 49 | ``` 50 | 51 | To cleanup a scope, import `Kill` and then call it on a scope: 52 | ```lua 53 | local Kill = Vinum.Kill 54 | 55 | Kill(scope) 56 | ``` 57 | 58 | Kill will cleanup the following: 59 | * Roblox Instances; will get destroyed 60 | * RBXScriptConnections; will get disconnected 61 | * functions; will get called 62 | * an OOP table with destroy/Destroy methods; the existing method will get called 63 | * an array composed of subtasks. 64 | * Vinum State Objects; triggers some special code to cleanup the value and related tasks. 65 | 66 | To derive a new scope from an older one (as in, creating a new scope with new custom constructors), import `DerivedScope` and then call it with the scope and a list of constructors: 67 | ```lua 68 | local DerivedScope = Vinum.DerivedScope 69 | 70 | local dScope= DerivedScope(scope, { 71 | hi = function()... 72 | }) 73 | 74 | dScope:Source(100) 75 | dScope:hi() 76 | ``` -------------------------------------------------------------------------------- /.lune/luneVendor/ecrTestKit.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | -- from https://github.com/centau/ecr/blob/main/test/testkit.luau 3 | -------------------------------------------------------------------------------- 4 | -- testkit.luau 5 | -- v0.7.3 6 | -------------------------------------------------------------------------------- 7 | 8 | local color = { 9 | white_underline = function(s: string) 10 | return `\27[1;4m{s}\27[0m` 11 | end, 12 | 13 | white = function(s: string) 14 | return `\27[37;1m{s}\27[0m` 15 | end, 16 | 17 | green = function(s: string) 18 | return `\27[32;1m{s}\27[0m` 19 | end, 20 | 21 | red = function(s: string) 22 | return `\27[31;1m{s}\27[0m` 23 | end, 24 | 25 | yellow = function(s: string) 26 | return `\27[33;1m{s}\27[0m` 27 | end, 28 | 29 | red_highlight = function(s: string) 30 | return `\27[41;1;30m{s}\27[0m` 31 | end, 32 | 33 | green_highlight = function(s: string) 34 | return `\27[42;1;30m{s}\27[0m` 35 | end, 36 | 37 | gray = function(s: string) 38 | return `\27[30;1m{s}\27[0m` 39 | end, 40 | } 41 | 42 | local function convert_units(unit: string, value: number): (number, string) 43 | local prefix_colors = { 44 | [4] = color.red, 45 | [3] = color.red, 46 | [2] = color.yellow, 47 | [1] = color.yellow, 48 | [0] = color.green, 49 | [-1] = color.red, 50 | [-2] = color.yellow, 51 | [-3] = color.green, 52 | [-4] = color.red 53 | } 54 | 55 | local prefixes = { 56 | [4] = "T", 57 | [3] ="G", 58 | [2] ="M", 59 | [1] = "k", 60 | [0] = " ", 61 | [-1] = "m", 62 | [-2] = "u", 63 | [-3] = "n", 64 | [-4] = "p" 65 | } 66 | 67 | local order = 0 68 | 69 | while value >= 1000 do 70 | order += 1 71 | value /= 1000 72 | end 73 | 74 | while value ~= 0 and value < 1 do 75 | order -= 1 76 | value *= 1000 77 | end 78 | 79 | if value >= 100 then 80 | value = math.floor(value) 81 | elseif value >= 10 then 82 | value = math.floor(value * 1e1) / 1e1 83 | elseif value >= 1 then 84 | value = math.floor(value * 1e2) / 1e2 85 | end 86 | 87 | return value, prefix_colors[order](prefixes[order] .. unit) 88 | end 89 | 90 | local WALL = color.gray "│" 91 | 92 | -------------------------------------------------------------------------------- 93 | -- Testing 94 | -------------------------------------------------------------------------------- 95 | 96 | type Test = { 97 | name: string, 98 | case: Case?, 99 | cases: { Case }, 100 | duration: number, 101 | error: { 102 | message: string, 103 | trace: string 104 | }? 105 | } 106 | 107 | type Case = { 108 | name: string, 109 | result: number, 110 | line: number? 111 | } 112 | 113 | local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4 114 | 115 | local skip: string? 116 | local test: Test? 117 | local tests: { Test } = {} 118 | 119 | local function output_test_result(test: Test) 120 | print(color.white(test.name)) 121 | 122 | for _, case in test.cases do 123 | local status = ({ 124 | [PASS] = color.green "PASS", 125 | [FAIL] = color.red "FAIL", 126 | [NONE] = color.yellow "NONE", 127 | [ERROR] = color.red "FAIL" 128 | })[case.result] 129 | 130 | local line = case.result == FAIL and color.red(`{case.line}:`) or "" 131 | 132 | print(`{status}{WALL} {line}{color.gray(case.name)}`) 133 | end 134 | 135 | if test.error then 136 | print(color.gray "error: " .. color.red(test.error.message)) 137 | print(color.gray "trace: " .. color.red(test.error.trace)) 138 | else 139 | print() 140 | end 141 | end 142 | 143 | local function CASE(name: string) 144 | assert(test, "no active test") 145 | 146 | local case = { 147 | name = name, 148 | result = NONE 149 | } 150 | 151 | test.case = case 152 | table.insert(test.cases, case) 153 | end 154 | 155 | local function CHECK(value: T, stack: number?): T 156 | assert(test, "no active test") 157 | local case = test.case 158 | 159 | if not case then 160 | CASE "" 161 | case = test.case 162 | end 163 | 164 | assert(case, "no active case") 165 | 166 | if case.result ~= FAIL then 167 | case.result = value and PASS or FAIL 168 | case.line = debug.info(stack and stack + 1 or 2, "l") 169 | end 170 | 171 | return value 172 | end 173 | 174 | local function TEST(name: string, fn: () -> ()) 175 | if skip and name ~= skip then return end 176 | 177 | local active = test 178 | assert(not active, "cannot start test while another test is in progress") 179 | 180 | test = { 181 | name = name, 182 | cases = {}, 183 | duration = 0 184 | }; assert(test) 185 | 186 | table.insert(tests, test) 187 | 188 | local start = os.clock() 189 | local err 190 | local success = xpcall(fn, function(m: string) 191 | err = { message = m, trace = debug.traceback(nil, 2) } 192 | end) 193 | test.duration = os.clock() - start 194 | 195 | if not test.case then CASE "" end 196 | assert(test.case, "no active case") 197 | 198 | if not success then 199 | test.case.result = ERROR 200 | test.error = err 201 | end 202 | 203 | test = nil 204 | end 205 | 206 | local function FINISH(): boolean 207 | local success = true 208 | local total_cases = 0 209 | local passed_cases = 0 210 | local duration = 0 211 | 212 | for _, test in tests do 213 | duration += test.duration 214 | for _, case in test.cases do 215 | total_cases += 1 216 | if case.result == PASS or case.result == NONE then 217 | passed_cases += 1 218 | else 219 | success = false 220 | end 221 | end 222 | 223 | output_test_result(test) 224 | end 225 | 226 | print(color.gray(string.format( 227 | `{passed_cases}/{total_cases} test cases passed in %.3f ms.`, 228 | duration*1e3 229 | ))) 230 | 231 | local fails = total_cases - passed_cases 232 | 233 | print( 234 | ( 235 | fails > 0 236 | and color.red 237 | or color.green 238 | )(`{fails} {fails == 1 and "fail" or "fails"}`) 239 | ) 240 | 241 | return success, table.clear(tests) 242 | end 243 | 244 | local function SKIP(name: string) 245 | assert(not test, "cannot skip during test") 246 | skip = name 247 | end 248 | 249 | -------------------------------------------------------------------------------- 250 | -- Benchmarking 251 | -------------------------------------------------------------------------------- 252 | 253 | type Bench = { 254 | time_start: number?, 255 | memory_start: number?, 256 | iterations: number? 257 | } 258 | 259 | local bench: Bench? 260 | 261 | function START(iter: number?): number 262 | local n = iter or 1 263 | assert(n > 0, "iterations must be greater than 0") 264 | assert(bench, "no active benchmark") 265 | assert(not bench.time_start, "clock was already started") 266 | 267 | bench.iterations = n 268 | bench.memory_start = gcinfo() 269 | bench.time_start = os.clock() 270 | return n 271 | end 272 | 273 | local function BENCH(name: string, fn: () -> ()) 274 | local active = bench 275 | assert(not active, "a benchmark is already in progress") 276 | 277 | bench = {}; assert(bench) 278 | 279 | ;(collectgarbage :: any)("collect") 280 | 281 | local mem_start = gcinfo() 282 | local time_start = os.clock() 283 | local err_msg: string? 284 | 285 | local success = xpcall(fn, function(m: string) 286 | err_msg = m .. debug.traceback(nil, 2) 287 | end) 288 | 289 | local time_stop = os.clock() 290 | local mem_stop = gcinfo() 291 | 292 | if not success then 293 | print(`{WALL}{color.red("ERROR")}{WALL} {name}`) 294 | print(color.gray(err_msg :: string)) 295 | else 296 | time_start = bench.time_start or time_start 297 | mem_start = bench.memory_start or mem_start 298 | 299 | local n = bench.iterations or 1 300 | local d, d_unit = convert_units("s", (time_stop - time_start) / n) 301 | local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3)) 302 | 303 | local function round(x: number): string 304 | return x > 0 and x < 10 and (x - math.floor(x)) > 0 305 | and string.format("%2.1f", x) 306 | or string.format("%3.f", x) 307 | end 308 | 309 | print(string.format( 310 | `%s %s %s %s{WALL} %s`, 311 | color.gray(round(d)), 312 | d_unit, 313 | color.gray(round(a)), 314 | a_unit, 315 | color.gray(name) 316 | )) 317 | end 318 | 319 | bench = nil 320 | end 321 | 322 | -------------------------------------------------------------------------------- 323 | -- Printing 324 | -------------------------------------------------------------------------------- 325 | 326 | local function print2(v: unknown) 327 | type Buffer = { n: number, [number]: string } 328 | type Cyclic = { n: number, [{}]: number } 329 | 330 | -- overkill concatenationless string buffer 331 | local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic) 332 | local TAB = " " 333 | local indent = table.concat(table.create(stack, TAB)) 334 | 335 | if type(value) == "string" then 336 | local n = str.n 337 | str[n + 1] = "\"" 338 | str[n + 2] = value 339 | str[n + 3] = "\"" 340 | str.n = n + 3 341 | elseif type(value) ~= "table" then 342 | local n = str.n 343 | str[n + 1] = value == nil and "nil" or tostring(value) 344 | str.n = n + 1 345 | elseif next(value) == nil then 346 | local n = str.n 347 | str[n + 1] = "{}" 348 | str.n = n + 1 349 | else -- is table 350 | local tabbed_indent = indent .. TAB 351 | 352 | if cyclic[value] then 353 | str.n += 1 354 | str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`) 355 | return 356 | else 357 | cyclic.n += 1 358 | cyclic[value] = cyclic.n 359 | end 360 | 361 | str.n += 3 362 | str[str.n - 2] = "{ " 363 | str[str.n - 1] = color.gray(tostring(cyclic[value])) 364 | str[str.n - 0] = "\n" 365 | 366 | local i, v = next(value, nil) 367 | while v ~= nil do 368 | local n = str.n 369 | str[n + 1] = tabbed_indent 370 | 371 | if type(i) ~= "string" then 372 | str[n + 2] = "[" 373 | str[n + 3] = tostring(i) 374 | str[n + 4] = "]" 375 | n += 4 376 | else 377 | str[n + 2] = tostring(i) 378 | n += 2 379 | end 380 | 381 | str[n + 1] = " = " 382 | str.n = n + 1 383 | 384 | tos(v, stack + 1, str, cyclic) 385 | 386 | i, v = next(value, i) 387 | 388 | n = str.n 389 | str[n + 1] = v ~= nil and ",\n" or "\n" 390 | str.n = n + 1 391 | end 392 | 393 | local n = str.n 394 | str[n + 1] = indent 395 | str[n + 2] = "}" 396 | str.n = n + 2 397 | end 398 | end 399 | 400 | local str = { n = 0 } 401 | local cyclic = { n = 0 } 402 | tos(v, 0, str, cyclic) 403 | print(table.concat(str)) 404 | end 405 | 406 | -------------------------------------------------------------------------------- 407 | -- Equality 408 | -------------------------------------------------------------------------------- 409 | 410 | local function shallow_eq(a: {}, b: {}): boolean 411 | if #a ~= #b then return false end 412 | 413 | for i, v in next, a do 414 | if b[i] ~= v then 415 | return false 416 | end 417 | end 418 | 419 | for i, v in next, b do 420 | if a[i] ~= v then 421 | return false 422 | end 423 | end 424 | 425 | return true 426 | end 427 | 428 | local function deep_eq(a: {}, b: {}): boolean 429 | if #a ~= #b then return false end 430 | 431 | for i, v in next, a do 432 | if type(b[i]) == "table" and type(v) == "table" then 433 | if deep_eq(b[i], v) == false then return false end 434 | elseif b[i] ~= v then 435 | return false 436 | end 437 | end 438 | 439 | for i, v in next, b do 440 | if type(a[i]) == "table" and type(v) == "table" then 441 | if deep_eq(a[i], v) == false then return false end 442 | elseif a[i] ~= v then 443 | return false 444 | end 445 | end 446 | 447 | return true 448 | end 449 | 450 | -------------------------------------------------------------------------------- 451 | -- Return 452 | -------------------------------------------------------------------------------- 453 | 454 | return { 455 | test = function() 456 | return TEST, CASE, CHECK, FINISH, SKIP 457 | end, 458 | 459 | benchmark = function() 460 | return BENCH, START 461 | end, 462 | 463 | print = print2, 464 | 465 | seq = shallow_eq, 466 | deq = deep_eq, 467 | 468 | color = color 469 | } -------------------------------------------------------------------------------- /gh-assets/Banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------