@rbxts/rl 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ## Wally
11
11
  Add the following to your `wally.toml`, in the `[dependencies]` section:
12
12
  ```toml
13
- rl = "wiam/rl@<version>"
13
+ rl = "wiam77/rl@<version>"
14
14
  ```
15
15
 
16
16
  Then run:
@@ -18,6 +18,11 @@ Then run:
18
18
  # wally install
19
19
  ```
20
20
 
21
+ ## npm
22
+ ```console
23
+ # npm install @rbxts/rl
24
+ ```
25
+
21
26
  # API Reference
22
27
  You can read the API reference in `rl`'s [Pesde package page](https://pesde.dev/packages/wiam/rl/latest/any/docs/api_ref).
23
28
 
package/docs/api_ref.md CHANGED
@@ -1,47 +1,74 @@
1
1
  # rl API Reference
2
2
  ## Types
3
- ### state\<T>
4
- The [state](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#state) type.
3
+ ### callback
4
+ Function which gets called when its [effect](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#effect) is triggered.
5
+ Takes the previous value of the state which triggered the effect, as well as its "index", determined by the order in which it appears within the function.
5
6
 
6
7
  ```luau
7
- type state<T> = (() -> T) & ((value : T, force : boolean?) -> T) & {
8
- value : T
9
- }
8
+ type callback<T = unknown> = ((old_value : T?, state_i : number?) -> unknown)
9
+ | ((old_value : T?, state_i : number?) -> ())
10
10
  ```
11
11
 
12
- ### callback
13
- Function which gets called when its [effect](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#effect) is triggered.
14
- Takes the previous value of the state which triggered the effect, as well as its "index", determined by the order in which it appears within the function.
12
+ ### state\<T>
13
+ The [state](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#state) type.
15
14
 
16
15
  ```luau
17
- type callback<T> = (old_value : T, state_i : number?) -> any
16
+ type state<T = unknown> = {
17
+ value : T,
18
+ always_force : boolean,
19
+ read callbacks : {[callback<T>] : number}
20
+ } & ( | (() -> T)
21
+ | ((new_value : T?, force : boolean?) -> T))
18
22
  ```
19
23
 
20
24
  ## Functions
21
25
  ### state
22
- Creates a new [state](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#state), with the given initial value, and whether to always [force](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#forcing), and returns it.
26
+ Returns a new [state](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#state), with the given initial value, and whether to always [force](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#forcing).
23
27
 
24
28
  ```luau
25
- function state<T>(value : T?, force : boolean?) : state<T>
29
+ function state<T>(value : T?, always_force : boolean?) : state<T>
26
30
 
27
- -- Example
28
31
  local health = state(100)
29
32
  local stamina = state(100, true)
30
33
  ```
31
34
 
35
+ ### effect
36
+ Creates a new [effect](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#effect), with the given [callback](https://pesde.dev/packages/wiam/rl/latest/any/docs/api_ref#callback), and returns its disconnect function.
37
+
38
+ ```luau
39
+ function effect<T>(callback : callback<T>) : () -> ()
40
+
41
+ effect(function()
42
+ print(`new health: {health()}`)
43
+ end)
44
+
45
+ health(10) -- "new health: 10"
46
+ health(10) -- doesn't print because the new value is the same as the current one
47
+ health(10, true) -- "new health: 10"; this prints because we forced it
48
+
49
+ local disconnect = effect(function()
50
+ print(`new stamina: {stamina()}`)
51
+ end)
52
+
53
+ stamina(90) -- "new stamina: 90"
54
+ stamina(90) -- "new stamina: 90"; this prints because `stamina` is always forced.
55
+ disconnect()
56
+
57
+ stamina(40) -- doesn't print because we disconnected the effect.
58
+ ```
59
+
32
60
  ### derive
33
- Wraps an Effect around the given callback which sets a newly created State's value
34
- to be the return value of the callback.
35
- Returns the created State, the Effect's disconnect function and a flag which can be ignored.
61
+ Wraps an Effect around the given Callback which sets a newly created State's value
62
+ to be the return value of the Callback.
63
+ Returns the created State and the Effect's disconnect function.
36
64
 
37
65
  This is useful for States which depend on the value of another State, since their value will
38
66
  be updated only when the State they depend on is also updated, and not every time it is read,
39
67
  as how would happen with a function which's return value depends on a State.
40
68
 
41
69
  ```luau
42
- function derive<T, U>(callback : () -> U) : (state<U>, () -> (), true)
70
+ function derive<T>(callback : (() -> T) | (() -> ())) : (state<T>, () -> ())
43
71
 
44
- -- Example
45
72
  -- without derive() (normal function)
46
73
  local half_health = function()
47
74
  print("some computation")
@@ -64,39 +91,12 @@ print(derive_half_health()) -- 10
64
91
  print(derive_half_health()) -- 10
65
92
  ```
66
93
 
67
- ### effect
68
- Creates a new [effect](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#effect), with the given [callback](https://pesde.dev/packages/wiam/rl/latest/any/docs/api_ref#callback) function, and returns its disconnect function.
69
-
70
- ```luau
71
- function effect<T>(callback : callback<T>) : () -> ()
72
-
73
- -- Example
74
- effect(function()
75
- print(`new health: {health()}`)
76
- end)
77
-
78
- health(10) -- "new health: 10"
79
- health(10) -- doesn't print because the new value is the same as the current one
80
- health(10, true) -- "new health: 10"; this prints because we forced it
81
-
82
- local disconnect = effect(function()
83
- print(`new stamina: {stamina()}`)
84
- end)
85
-
86
- stamina(90) -- "new stamina: 90"
87
- stamina(90) -- "new stamina: 90"; this prints because `stamina` is always forced.
88
- disconnect()
89
-
90
- stamina(40) -- doesn't print because we disconnected the effect.
91
- ```
92
-
93
94
  ### batch
94
95
  Batches any State write operation up until the function is done running.
95
96
 
96
97
  ```luau
97
- function batch(f : () -> any) : ()
98
+ function batch(f : (() -> unknown) | () -> ()) : ()
98
99
 
99
- -- Example
100
100
  batch(function()
101
101
  health(420)
102
102
  health(1337, true)
@@ -104,18 +104,15 @@ batch(function()
104
104
  end)
105
105
 
106
106
  -- "new health: 69"
107
-
108
107
  ```
109
108
 
110
109
  ### scope
111
- Returns a function which calls the given functions.
112
- Any non-function or function which precedes a "true" value by 2 indices is going to be skipped (the second happens because of derives).
113
- This is intended to be used for disconnecting Effects in bulk.
110
+ Wraps the given callbacks into Effects.
111
+ Returns a function which disconnects all the created Effects.
114
112
 
115
113
  ```luau
116
- function scope<T>(... : T) : () -> ()
114
+ function scope(... : callback) : () -> ()
117
115
 
118
- -- Example
119
116
  -- without scope
120
117
  local disconnect_health = effect(function()
121
118
  print(`your health is : {health()}`)
@@ -132,12 +129,12 @@ disconnect_stamina()
132
129
 
133
130
  -- with scope
134
131
  local vitals_scope = scope(
135
- effect(function()
132
+ function()
136
133
  print(`your health is : {health()}`)
137
- end),
138
- effect(function()
134
+ end,
135
+ function()
139
136
  print(`your stamina is : {stamina()}`)
140
- end)
137
+ end
141
138
  )
142
139
 
143
140
  -- ...
@@ -146,14 +143,13 @@ vitals_scope()
146
143
  ```
147
144
 
148
145
  ### branch
149
- Alternative `if`-like function to be used within effects. Learn more about it in the [tracking section of the Concepts page](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#tracking).
146
+ `if`-like function to be used within effects. Learn more about it in the [tracking section of the intro page](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#tracking).
150
147
  Pass a condition to be checked, and after it a function to be run if it's successful.
151
148
  The last value, if a function, and if not after a condition, will be threated as the "else" branch.
152
149
 
153
150
  ```luau
154
- function branch(...) : ()
151
+ function branch(... : any) : ()
155
152
 
156
- -- Example
157
153
  -- A single effect for the same functionality as the examples above, thanks to the use of state_i and the branching function.
158
154
  effect(function(_, state_i)
159
155
  branch(
@@ -168,35 +164,40 @@ end)
168
164
  ```
169
165
 
170
166
  ### mirror
171
- Takes a table and for every function or state, creates an effect which automatically updates the element to be either the function's return value, or, if a state, to its value.
172
- The created effects' disconnect functions are put at t\["disconnect"..k], where k is either the key, if it's a string, or the index at which it was iterated over.
173
- Returns the given table, as well as a function which disconnects all created effects.
167
+ Iterates over the given table and creates an Effect for each key that is
168
+ either a function or a State, updating the key's value to be the return value of the
169
+ function, or the State's value.
170
+ Returns table containing each key's Effect's disconnect function.
174
171
 
175
172
  ```luau
176
- function mirror<K, V>(t : {[K] : V}) : ({[K] : V}, () -> ())
173
+ type function indexer(t : type) : type
174
+ return (t:indexer() :: any).index
175
+ end
176
+
177
+ type identifier<T> = keyof<T> | indexer<T>
178
+
179
+ function mirror<T>(t : T) : (T, {[identifier<T>] : () -> ()})
177
180
 
178
- -- Example
179
181
  local name = state("john")
180
- local data = mirror {
181
- username = name,
182
+ local data, data_disconnects = mirror {
183
+ username = name
182
184
  }
183
185
 
184
186
  print(data.username) -- "john"
185
187
  name("joe")
186
188
  print(data.username) -- "joe"
187
- data.disconnect_username()
189
+ data_disconnects.username()
188
190
 
189
191
  name("doe")
190
- print({data.username}) -- "joe"; didn't update because we disconnected the effect
192
+ print({data.username}) -- "joe"; didn't update because we disconnected the Effect.
191
193
  ```
192
194
 
193
195
  ### cleanup
194
- Disconnects all [effects](https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts#effect).
196
+ Disconnects all [effects](https://pesde.dev/packages/wiam/rl/latest/any/docs/intro#effect).
195
197
 
196
198
  ```luau
197
199
  function cleanup() : ()
198
200
 
199
- -- Example
200
201
  cleanup()
201
202
  health(77) -- doesn't print because no effect is connected to this state (we just disconnected)
202
203
  ```
@@ -11,10 +11,10 @@ Instead, `rl` solves this by providing the `branch` function, which works like s
11
11
 
12
12
  ## State
13
13
  A State is a container for a value.\
14
- Internally, it is an userdata that has a metatable with the `__index` and `__newindex` metamethods, which permit reading and writing the value with special mechanisms.\
15
14
  Call it with no arguments for reading with tracking.\
16
15
  Call with arguments for writing, where the first argument is the new value, and the second one is whether to force for this specific call.\
17
- Access the `.value` field for reading without tracking. Note that this is read-only, and attempting to modify it will cause an error. `rawset` doesn't work either, since an userdata is not a table.
16
+ Access the `.value` field for reading without tracking.
17
+ Modify the `.value` field for writing without tracking.
18
18
 
19
19
  ## Forcing
20
20
  Forcing is when you write a new value to a State, and always trigger its Effects, even when the new value is the same as the old one.\
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rbxts/rl",
3
- "version": "0.1.11",
4
- "description": "simple reactive library",
3
+ "version": "0.2.0",
4
+ "description": "Simple reactive library. Docs: https://pesde.dev/packages/wiam/rl/latest/any/docs/intro",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -12,5 +12,11 @@
12
12
  "license": "GPL-3.0-or-later",
13
13
  "author": "wi_am",
14
14
  "main": "src/init.luau",
15
- "typings": "src/index.d.ts"
15
+ "typings": "src/index.d.ts",
16
+ "files": [
17
+ "src/",
18
+ "docs/",
19
+ "COPYING",
20
+ "default.project.json"
21
+ ]
16
22
  }
package/src/index.d.ts CHANGED
@@ -1,74 +1,87 @@
1
- export type state<T> = {
2
- (new_value: T, force?: boolean): T
3
- (): T
4
- readonly value: T
5
- }
1
+ declare namespace rl {
2
+ type callback<T = unknown> = (old_value?: T, state_i?: number) => unknown
6
3
 
7
- export type callback<T> = (old_value: T, state_i?: number) => any
4
+ export type state<T = unknown> = {
5
+ (new_value: T, force?: boolean): T
6
+ (): T
7
+ value: T
8
+ always_force: boolean
9
+ readonly callbacks: Map<callback<T>, number>
10
+ }
8
11
 
9
- /**
10
- * Disconnects all Effects.
11
- */
12
- export function cleanup(): void
12
+ /**
13
+ * Creates a State.
14
+ * @param initial_value? T -- The initial value for the State, can be nil.
15
+ * @param always_force? boolean -- Whether Effects connected to this State will always trigger, even when the value didn't change.
16
+ * @returns state<T> -- The created State. Call with no arguments for reading with tracking,
17
+ * call with arguments for writing (2nd argument is for forcing),
18
+ * read .value field for reading without tracking,
19
+ * write .value field for writing without triggering Effects.
20
+ */
21
+ function state<T>(initial_value: T, always_force?: boolean): state<T>
13
22
 
14
- /**
15
- * Creates a State.
16
- *
17
- * @param value T -- Initial value.
18
- * @param force? boolean -- Whether Effects connected to this State will trigger, despite the fact that the value didn't change.
19
- * @returns state<T> -- State. Call with no arguments for reading with tracking, call with arguments for writing (2nd argument is for forcing), read .value field for reading without tracking.
20
- */
21
- export function state<T>(value: T, force?: boolean): state<T>
23
+ /**
24
+ * Creates an Effect, a binding of the given function to the event of States read (by calling) within it being updated.
25
+ *
26
+ * @param callback callback<T> -- The function to be bound.
27
+ * @returns () => void -- Function which disconnects the Effect from the States.
28
+ */
29
+ function effect<T>(callback: callback<T>): () => void
22
30
 
23
- /**
24
- * Batches any State write operation up until the function is done running.
25
- * @param f () => any -- The function.
26
- */
27
- export function batch(f: () => any): void
31
+ /**
32
+ * Wraps an Effect around the given Callback which sets a newly created State's value
33
+ * to be the return value of the Callback.
34
+ * Returns the created State and the Effect's disconnect function.
35
+ *
36
+ * This is useful for States which depend on the value of another State, since their value will
37
+ * be updated only when the State they depend on is also updated, and not every time it is read,
38
+ * as how would happen with a function which's return value depends on a State.
39
+ *
40
+ * @param callback () => T -- The callback.
41
+ * @returns state<T> -- The State that was created.
42
+ * @returns () => void -- The created Effect's disconnect function.
43
+ */
44
+ function derive<T>(callback: () => T): [state<T>, () => void]
28
45
 
29
- /**
30
- * Creates an Effect, a binding of the given function to the event of States read (by calling) within it being updated.
31
- *
32
- * @param callback callback<T> -- The function to be bound.
33
- * @returns () => void -- Function which disconnects the Effect from the States.
34
- */
35
- export function effect<T>(callback: callback<T>): () => void
46
+ /**
47
+ * Batches any State write operation up until the function is done running.
48
+ *
49
+ * @param f () => unknown -- The function.
50
+ */
51
+ function batch(f: () => unknown): void
36
52
 
37
- /**
38
- * Returns a function which calls the given functions.
39
- * Any non-function or function which precedes a "true" value by 2 indices is going to be skipped (the second happens because of derives).
40
- * This is intended to be used for disconnecting Effects in bulk.
41
- * @param ... () => () -- The functions.
42
- * @returns () => () -- The function caller.
43
- */
44
- export function scope<T>(...t : T[]): () => void
53
+ /**
54
+ * Wraps the given callbacks into Effects.
55
+ * Returns a function which disconnects all the created Effects.
56
+ *
57
+ * @param ...args { [K in keyof T]: callback<T[K]> } -- The callbacks.
58
+ * @returns () => void -- The disconnecter.
59
+ */
60
+ function scope<T extends any[]>(...args: { [K in keyof T]: callback<T[K]> }): () => void
45
61
 
46
- /**
47
- * Wraps an Effect around the given callback which sets a newly created State's value
48
- * to be the return value of the callback.
49
- * Returns the created State, the Effect's disconnect function and a flag which can be ignored.
50
- * This is useful for States which depend on the value of another State, since their value will
51
- * be updated only when the State they depend on is also updated, and not every time it is read,
52
- * as how would happen with a function which's return value depends on a State.
53
- * @param callback () => U -- the callback
54
- * @returns state<U> -- the State that was created
55
- * @returns () => void -- the created Effect's disconnect function
56
- * @returns true -- a flag used internally by rl.scope(); can be ignored.
57
- */
58
- export function derive<T, U>(callback: () => U) : [state<U>, () => void, true]
62
+ /**
63
+ * Branching function to be used within Effects.
64
+ * Pass a condition to be checked, and after it a function to be run if it's successful.
65
+ * The last value, if a function, and if not after a condition, will be threated as the "else" branch.
66
+ *
67
+ * @param ...a any -- The values and the branches.
68
+ */
69
+ function branch(...a: any): void
59
70
 
60
- /**
61
- * Branching function to be used within Effects.
62
- * Pass a condition to be checked, and after it a function to be run if it's successful.
63
- * The last value, if a function, and if not after a condition, will be threated as the "else" branch.
64
- */
65
- export function branch(...a : any) : void
71
+ /**
72
+ * Iterates over the given table and creates an Effect for each key that is
73
+ * either a function or a State, updating the key's value to be the return value of the
74
+ * function, or the State's value.
75
+ * Returns table containing each key's Effect's disconnect function.
76
+ *
77
+ * @param t T -- The table to iterate.
78
+ * @return T -- The given table.
79
+ * @return {[identifier<T>] : () -> ()} -- The table containing the disconnect functions.
80
+ */
81
+ function mirror<K, V>(t: Map<K, V>): [Map<K, V>, Map<K, () => void>]
66
82
 
67
- /**
68
- * Creates an effect for every function or State within the given table, which updates the key to either the return value of the function, or the value of the State.
69
- * Each Effect's disconnect function gets put in the table as t["disconnect_"..key], when key is a string, or as t["disconnect"_..i], where i is the numerical index at which the key was iterated over.
70
- * @param t {[K] : V} -- The table.
71
- * @return {[K] : V} -- The same table.
72
- * @returns () => void -- Function which disconnects and eliminates each disconnect function of the created Effects.
73
- */
74
- export function mirror<K, V>(t : Map<K, V>): [Map<K, V>, () => void]
83
+ /**
84
+ * Disconnects all Effects.
85
+ */
86
+ function cleanup(): void
87
+ }
package/src/init.luau CHANGED
@@ -1,25 +1,42 @@
1
1
  --!strict
2
+ --[[
3
+ TODO:
4
+ When creating a State, if its variable type is annotated,
5
+ and the type of the value passed to rl.state() doesn't match,
6
+ there should be a type error:
7
+ local health : rl.state<number> = rl.state("i'm not a number") -- doesn't error
2
8
 
3
- local rl = {}
4
-
5
- export type state<T> = (() -> T) & ((new_value : T, force : boolean?) -> T) & {
6
- value : T
7
- }
8
-
9
- export type callback<T> = (old_value : T, state_i : number?) -> any
9
+ When modifying a State, if the value passed to state() doesn't match its type,
10
+ there should be a type error:
11
+ local health : rl.state<number> = rl.state(100)
12
+ health("i'm not a number")
10
13
 
11
- local batching = false
12
- -- {[state] : {{[callback] = true}, old_value, state_i}}
13
- local batched : {[state<unknown>] : {any}} = {}
14
-
15
- local current_callback : callback<unknown>? = nil
16
- local effects : {[callback<unknown>] : number} = {}
14
+ - The 2 bugs above could be fixed by using the commented out state<T> type.
15
+ ]]--
17
16
 
18
- local function newindex_err()
19
- error("Cannot mutate a State by editing it's .value!")
20
- end
17
+ local rl = {}
21
18
 
22
- local function check<T>(v : T) : T
19
+ export type callback<T = unknown> = ((old_value : T?, state_i : number?) -> unknown)
20
+ | ((old_value : T?, state_i : number?) -> ())
21
+
22
+ -- Currently broken, might get fixed in the future.
23
+ -- export type state<T = unknown> = setmetatable<{
24
+ -- value : T,
25
+ -- read callbacks : {[callback<T>] : number},
26
+ -- read always_force : boolean
27
+ -- }, {
28
+ -- __call : (() -> T)
29
+ -- & ((new_value : T?, force : boolean?) -> T)
30
+ -- }>
31
+
32
+ export type state<T = unknown> = {
33
+ value : T,
34
+ always_force : boolean,
35
+ read callbacks : {[callback<T>] : number}
36
+ } & ( | (() -> T)
37
+ | ((new_value : T?, force : boolean?) -> T))
38
+
39
+ local function is_function<T>(v : T) : T
23
40
  local vt = type(v)
24
41
  assert(vt == "function", `Expected type 'function', got '{vt}'`)
25
42
  return v
@@ -29,97 +46,76 @@ local warn = warn or function(msg : string) : ()
29
46
  print(`\x1b[33m{msg}\x1b[0m`)
30
47
  end
31
48
 
32
- --[=[
33
- Disconnects all Effects.
34
- ]=]
35
- function rl.cleanup() : ()
36
- table.clear(effects)
37
- end
49
+ local batching = false
50
+ local batched : {[state] : {any}} = {} -- {[state] : {[1] : {[callback] = true}, [2] : old_value, [3] : state_i}}
38
51
 
39
- --[=[
40
- Creates a State.
41
-
42
- @param value T -- Initial value.
43
- @param force boolean? -- Whether Effects connected to this State will trigger, despite the fact that the value didn't change.
44
- @return state<T> -- State. Call with no arguments for reading with tracking, call with arguments for writing (2nd argument is for forcing), read .value field for reading without tracking.
45
- ]=]
46
- function rl.state<T>(value : T?, force : boolean?) : state<T>
47
- local callbacks : {[callback<unknown>] : number} = {}
48
- local self = newproxy(true)
49
- local mt = getmetatable(self)
50
- mt.__newindex = newindex_err
51
- mt.__index = function(_, k)
52
- assert(k == "value", `Expected key 'value', got '{k}'!`)
53
- return value
54
- end
52
+ local current_callback : callback? = nil
53
+ local effects : {[callback] : number} = {}
54
+
55
+ local function state__call<T>(self : state<T>, new_value : T?, force : boolean?) : T
56
+ local old_value = self.value
57
+ local callbacks = self.callbacks
55
58
 
56
- mt.__call = function(... : T) : T
57
- local argv = {...}
58
- local old_value = value
59
- local new_value = argv[2]
60
-
61
- if #argv == 1 then
62
- if current_callback and not callbacks[current_callback] then
63
- effects[current_callback] += 1
64
- callbacks[current_callback] = effects[current_callback]
65
- end
66
- elseif value :: any ~= new_value or force == true or argv[3] == true then
67
- value = new_value
59
+ if not (new_value or force) then
60
+ if current_callback and not callbacks[current_callback] then
61
+ effects[current_callback] += 1
62
+ callbacks[current_callback] = effects[current_callback]
63
+ end
64
+ elseif old_value ~= new_value or self.always_force or force then
65
+ self.value = new_value
68
66
 
69
- for callback, state_i in callbacks do
70
- if not effects[callback] then
71
- callbacks[callback] = nil
72
- continue
73
- end
67
+ for callback, state_i in callbacks do
68
+ if not effects[callback] then
69
+ callbacks[callback] = nil
70
+ continue
71
+ end
74
72
 
75
- if batching then
76
- local state_batch = batched[self]
77
- if state_batch then
78
- local x = state_batch[1]
79
- if not x[callback] then
80
- x[callback] = true
81
- end
82
-
83
- state_batch[2] = old_value
84
- else
85
- batched[self] = {{[callback] = true}, old_value, state_i}
73
+ if batching then
74
+ local state_batch = batched[self]
75
+ if state_batch then
76
+ local x = state_batch[1]
77
+ if not x[callback] then
78
+ x[callback] = true
86
79
  end
80
+
81
+ state_batch[2] = old_value
87
82
  else
88
- callback(old_value, state_i)
83
+ batched[self] = {{[callback] = true}, old_value, state_i}
89
84
  end
85
+ else
86
+ callback(old_value, state_i)
90
87
  end
91
88
  end
92
-
93
- return value
94
89
  end
95
90
 
96
- return self
91
+ return self.value
97
92
  end
98
93
 
99
94
  --[=[
100
- Batches any State write operation up until the function is done running.
101
-
102
- @param f () -> any -- The function.
95
+ Creates a State.
96
+
97
+ @param initial_value T? -- The initial value for the State, can be nil.
98
+ @param always_force boolean? -- Whether Effects connected to this State will always trigger, even when the value didn't change.
99
+ @return state<T> -- The created State. Call with no arguments for reading with tracking,
100
+ call with arguments for writing (2nd argument is for forcing),
101
+ read .value field for reading without tracking,
102
+ write .value field for writing without triggering Effects.
103
103
  ]=]
104
- function rl.batch(f : () -> any) : ()
105
- batching = true
106
- f()
107
-
108
- for _, result in batched do
109
- for callback in result[1] do
110
- callback(result[2], result[3])
111
- end
112
- end
113
-
114
- batching = false
115
- table.clear(batched :: {any})
104
+ function rl.state<T>(initial_value : T?, always_force : boolean?) : state<T>
105
+ return setmetatable({
106
+ value = initial_value,
107
+ callbacks = {},
108
+ always_force = always_force or false
109
+ }, {
110
+ __call = state__call
111
+ }) :: any
116
112
  end
117
113
 
118
114
  --[=[
119
115
  Creates an Effect, a binding of the given function to the event of States read (by calling) within it being updated.
120
116
 
121
117
  @param callback callback<T> -- The function to be bound.
122
- @return disconnect () -> () -- Function which disconnects the Effect from the States.
118
+ @return () -> () -- Function which disconnects the Effect from the States.
123
119
  ]=]
124
120
  function rl.effect<T>(callback : callback<T>) : () -> ()
125
121
  if current_callback then
@@ -142,106 +138,125 @@ function rl.effect<T>(callback : callback<T>) : () -> ()
142
138
  end
143
139
 
144
140
  --[=[
145
- Returns a function which calls the given functions.
146
- Any non-function or function which precedes a "true" value by 2 indices is going to be skipped (the second happens because of derives).
147
- This is intended to be used for disconnecting Effects in bulk.
141
+ Wraps an Effect, around the given Callback, which sets a newly created State's value
142
+ to be the return value of the Callback.
143
+ Returns the created State and the Effect's disconnect function.
144
+
145
+ This is useful for States which depend on the value of another State, since their value will
146
+ be updated only when the State they depend on is also updated, and not every time it is read,
147
+ as how would happen with a function which's return value depends on a State.
148
+
149
+ @param callback () -> T -- The callback.
150
+ @return state<T> -- The State that was created.
151
+ @return () -> () -- The created Effect's disconnect function.
152
+ ]=]
153
+ function rl.derive<T>(callback : (() -> T) | (() -> ())) : (state<T>, () -> ())
154
+ local state = rl.state()
155
+ return state, rl.effect(function()
156
+ state(callback())
157
+ end)
158
+ end
159
+
160
+ --[=[
161
+ Batches any State write operation up until the given function is done running.
148
162
 
149
- @param ... () -> () -- The functions.
150
- @return f () -> () -- The function caller.
163
+ @param f (() -> unknown) | () -> () -- The function.
151
164
  ]=]
152
- function rl.scope<T>(... : T) : () -> ()
153
- local t = {...}
154
- return function()
155
- for i = 1, #t do
156
- local v = t[i]
157
- if type(v) ~= "function" or t[i + 2] == true then continue end
158
- v()
165
+ function rl.batch(f : (() -> unknown) | () -> ()) : ()
166
+ batching = true
167
+ f()
168
+
169
+ for _, result in batched do
170
+ for callback in result[1] :: {[callback] : true} do
171
+ callback(result[2], result[3] :: number)
159
172
  end
160
173
  end
174
+
175
+ batching = false
176
+ table.clear(batched)
161
177
  end
162
178
 
163
179
  --[=[
164
- Wraps an Effect around the given callback which sets a newly created State's value
165
- to be the return value of the callback.
166
- Returns the created State, the Effect's disconnect function and a flag which can be ignored.
167
-
168
- This is useful for States which depend on the value of another State, since their value will
169
- be updated only when the State they depend on is also updated, and not every time it is read,
170
- as how would happen with a function which's return value depends on a State.
180
+ Wraps the given Callbacks into Effects.
181
+ Returns a function which disconnects all the created Effects.
171
182
 
172
- @param callback () -> U -- the callback
173
- @return derived_state state<U> -- the State that was created
174
- @return disconnect () -> -- the created Effect's disconnect function
175
- @return flag true -- a flag used internally by rl.scope(); can be ignored.
183
+ @param ... () -> () -- The Callbacks.
184
+ @return () -> () -- The disconnecter.
176
185
  ]=]
177
- function rl.derive<T, U>(callback : () -> U) : (state<U>, () -> (), true)
178
- local cache = rl.state()
179
- return cache, rl.effect(function()
180
- cache(callback())
181
- end), true
186
+ function rl.scope(... : callback) : () -> ()
187
+ local disconnects = {}
188
+ for _, callback in {...} do
189
+ table.insert(disconnects, rl.effect(callback))
190
+ end
191
+
192
+ return function()
193
+ for _, disconnect in disconnects do
194
+ disconnect()
195
+ end
196
+ end
182
197
  end
183
198
 
184
199
  --[=[
185
200
  Branching function to be used within Effects.
186
201
  Pass a condition to be checked, and after it a function to be run if it's successful.
187
202
  The last value, if a function, and if not after a condition, will be threated as the "else" branch.
203
+
204
+ @param ... any -- The values and the branches.
188
205
  ]=]
189
- function rl.branch(...) : ()
206
+ function rl.branch(... : any) : ()
190
207
  local args = table.pack(...)
191
208
  local n = args.n
192
- assert(n >= 2, "Expected 2 or more arguments!")
209
+ assert(n > 1, "Expected at least 2 arguments!")
193
210
 
194
211
  local i = 1
195
212
  while i + 1 <= n do
196
213
  if args[i] or current_callback then
197
- check(args[i + 1])()
214
+ is_function(args[i + 1])()
198
215
  end
199
216
 
200
217
  i += 2
201
218
  end
202
219
 
203
220
  if i <= n then
204
- check(args[i])()
221
+ is_function(args[i])()
205
222
  end
206
223
  end
207
224
 
208
- --[=[
209
- Creates an effect for every function or State within the given table, which updates the key to either the return value of the function, or the value of the State.
210
- Each Effect's disconnect function gets put in the table as t["disconnect_"..key], when key is a string, or as t["disconnect"_..i], where i is the numerical index at which the key was iterated over.
225
+ type function indexer(t : type) : type
226
+ return (t:indexer() :: any).index
227
+ end
211
228
 
212
- @param t {[K] : V} -- The table.
213
- @return t {[K] : V} -- The same table.
214
- @return disconnect_all () -> () -- Function which disconnects and eliminates each disconnect function of the created Effects.
229
+ -- couldn't think of a better name
230
+ type identifier<T> = keyof<T> | indexer<T>
231
+
232
+ --[=[
233
+ Iterates over the given table and creates an Effect for each key that is
234
+ either a function or a State, updating the key's value to be the return value of the
235
+ function, or the State's value.
236
+ Returns table containing each key's Effect's disconnect function.
237
+
238
+ @param t T -- The table to iterate.
239
+ @return T -- The given table.
240
+ @return {[identifier<T>] : () -> ()} -- The table containing the disconnect functions.
215
241
  ]=]
216
- function rl.mirror<K, V>(t : {[K] : V}) : ({[K] : V}, () -> ())
217
- local disconnects : {[number | string] : () -> ()} = {}
218
-
219
- for k, v in t do
220
- local v_type = type(v)
221
- if v_type == "function" or (v_type == "table" and (v :: {[any] : any}).value) then
222
- local disconnect = rl.effect(function()
223
- t[k] = v()
224
- end)
225
-
226
- local k_type = type(k)
227
- if k_type == "string" then
228
- disconnects[k] = disconnect
229
- else
230
- table.insert(disconnects :: {any}, disconnect)
231
- end
232
- end
242
+ function rl.mirror<T>(t : T) : (T, {[identifier<T>] : () -> ()})
243
+ local disconnects : {[identifier<T>] : () -> ()} = {}
244
+ for k, v in (t :: {[identifier<T>] : unknown}) do
245
+ local mt = getmetatable(v :: any)
246
+ if type(v) ~= "function" and not (mt and mt.__call == state__call) then continue end
247
+ disconnects[k] = rl.effect(function()
248
+ (t :: {[identifier<T>] : unknown})[k] = (v :: () -> ())()
249
+ end)
233
250
  end
234
251
 
235
- for name, disconnect in disconnects do
236
- t["disconnect_"..name] = disconnect
237
- end
252
+ return t, disconnects
253
+ end
238
254
 
239
- return t, function()
240
- for name, disconnect in disconnects do
241
- disconnect()
242
- t["disconnect_"..name] = nil
243
- end
244
- end
255
+ --[=[
256
+ Disconnects all Effects.
257
+ ]=]
258
+ function rl.cleanup() : ()
259
+ table.clear(effects)
245
260
  end
246
261
 
247
262
  return rl
package/PESDE_README.md DELETED
@@ -1,39 +0,0 @@
1
- # rl
2
- `rl` is a simple reactive library for Luau, inspired by [Vide](https://centau.github.io/vide)'s sources and effects system.
3
-
4
- # API Reference
5
- You can read `rl`'s API reference [here](https://pesde.dev/packages/wiam/rl/latest/any/docs/api_ref).
6
-
7
- # Demo
8
- ```luau
9
- --!strict
10
-
11
- local rl = require("path/to/rl")
12
- local s = rl.state
13
- local e = rl.effect
14
-
15
- local name = s("john", true)
16
- local login_year = s(2020)
17
-
18
- local login_d = e(function(last_login : number)
19
- print(`{name.value}, it's been {login_year() - (last_login or 0)} years!`)
20
- end)
21
-
22
- login_year(2025) -- "wiam, it's been 5 years!"
23
- login_year(2025) -- doesn't print because value didn't change
24
- login_year(2025, true) -- "wiam, it's been 0 years!" - this prints since it's forced
25
-
26
- name("doe") -- doesn't print because we are reading with name.value, and not name()
27
-
28
- e(function(old_name : string)
29
- print(`hey {old_name}, your name has been changed to '{name()}'`)
30
- end)
31
-
32
- name("jane") -- "hey doe, your name has been changed to jane"
33
- name("jane") -- "hey jane, your name has been changed to jane" - this one is forced by default
34
-
35
- login_d()
36
- login_year(2026) -- doesn't print because the effect was disconnected
37
-
38
- rl.cleanup() -- disconnect all effects
39
- ```
package/base_pesde.toml DELETED
@@ -1,10 +0,0 @@
1
- name = "wiam/rl"
2
- version = "0.1.11"
3
- description = "Simple reactive library for Luau."
4
- authors = ["wiam"]
5
- repository = "https://codeberg.org/wiam/rl"
6
- license = "GPL-3.0-or-later"
7
- includes = ["src/*", "pesde.toml", "COPYING", "README.md", "docs/*"]
8
-
9
- [indices]
10
- default = "https://github.com/pesde-pkg/index"
package/build.luau DELETED
@@ -1,30 +0,0 @@
1
- --!strict
2
-
3
- local fs = require("@lune/fs")
4
- local serde = require("@lune/serde")
5
-
6
- local TARGETS = {"luau", "lune", "roblox"}
7
- local INCLUDES = {"src", "COPYING", "PESDE_README.md", "docs"}
8
-
9
- local base_manifest = fs.readFile("base_pesde.toml")
10
-
11
- if fs.isDir("build") then
12
- fs.removeDir("build")
13
- end
14
-
15
- fs.writeDir("build")
16
-
17
- for _, target in TARGETS do
18
- local out_dir = "build/"..target
19
- fs.writeDir(out_dir)
20
-
21
- for _, include in INCLUDES do
22
- fs.copy(include, out_dir.."/"..if include == "PESDE_README.md" then "README.md" else include)
23
- end
24
-
25
- fs.writeFile(out_dir.."/pesde.toml", base_manifest.."\n"..serde.encode("toml", {target = {
26
- environment = target,
27
- lib = "src/init.luau",
28
- build_files = target == "roblox" and {"src"}
29
- }}, true))
30
- end
package/pesde.toml DELETED
@@ -1,14 +0,0 @@
1
- name = "wiam/rl_root"
2
- version = "1.0.0"
3
- license = "GPL-3.0-or-later"
4
- workspace_members = ["build/*"]
5
- private = true
6
-
7
- [target]
8
- environment = "lune"
9
-
10
- [scripts]
11
- build = "./build.luau"
12
-
13
- [indices]
14
- default = "https://github.com/pesde-pkg/index"
package/publish.sh DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/sh
2
-
3
- pesde run build && pesde update && pesde publish -y && wally publish
package/rokit.toml DELETED
@@ -1,4 +0,0 @@
1
- [tools]
2
- wally = "UpliftGames/wally@0.3.2"
3
- rojo = "rojo-rbx/rojo@7.6.1"
4
- lune = "lune-org/lune@0.10.4"
package/wally.toml DELETED
@@ -1,9 +0,0 @@
1
- [package]
2
- name = "wiam77/rl"
3
- description = "Simple reactive library for Luau. Docs: https://pesde.dev/packages/wiam/rl/latest/any/docs/concepts"
4
- version = "0.1.11"
5
- license = "GPL-3.0-or-later"
6
- registry = "https://github.com/UpliftGames/wally-index"
7
- realm = "shared"
8
- include = ["src", "src/*", "wally.toml", "COPYING", "README.md", "default.project.json"]
9
- exclude = ["**"]