@quenty/rx 13.11.1 → 13.12.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/CHANGELOG.md +23 -0
- package/package.json +6 -6
- package/src/Shared/Observable.lua +1 -1
- package/src/Shared/ObservableSubscriptionTable.lua +42 -8
- package/src/Shared/Rx.lua +109 -18
- package/src/Shared/Subscription.lua +31 -21
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [13.12.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/rx@13.11.1...@quenty/rx@13.12.0) (2024-11-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Better stack traces ([247ce9b](https://github.com/Quenty/NevermoreEngine/commit/247ce9bd97753dec8c8fd6674f93cba2c7deca05))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Add ObservableSubscriptionTable:Fail(key) ([0bb3fae](https://github.com/Quenty/NevermoreEngine/commit/0bb3faeaff104fe924122a7274a47539cd88350a))
|
|
17
|
+
* Add unfinished observable sorted list ([c7e9817](https://github.com/Quenty/NevermoreEngine/commit/c7e9817f07c9431e5f7cdf1fa2e700d3b3277f60))
|
|
18
|
+
* Optimize memory and perf with combineLatestDefer ([3bed556](https://github.com/Quenty/NevermoreEngine/commit/3bed55667c3c795402260575f57132404b906112))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Performance Improvements
|
|
22
|
+
|
|
23
|
+
* Check canFire() and cache result ([34fc3d9](https://github.com/Quenty/NevermoreEngine/commit/34fc3d98ca9d1dbece1d1f82f40519e986cdd5fb))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
## [13.11.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/rx@13.11.0...@quenty/rx@13.11.1) (2024-11-04)
|
|
7
30
|
|
|
8
31
|
**Note:** Version bump only for package @quenty/rx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/rx",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.12.0",
|
|
4
4
|
"description": "Quenty's reactive library for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -28,18 +28,18 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@quenty/cancellabledelay": "^3.5.0",
|
|
31
|
-
"@quenty/canceltoken": "^11.
|
|
31
|
+
"@quenty/canceltoken": "^11.9.0",
|
|
32
32
|
"@quenty/ducktype": "^5.7.1",
|
|
33
33
|
"@quenty/loader": "^10.7.1",
|
|
34
34
|
"@quenty/maid": "^3.4.0",
|
|
35
|
-
"@quenty/promise": "^10.
|
|
36
|
-
"@quenty/signal": "^7.
|
|
37
|
-
"@quenty/symbol": "^3.
|
|
35
|
+
"@quenty/promise": "^10.8.0",
|
|
36
|
+
"@quenty/signal": "^7.9.0",
|
|
37
|
+
"@quenty/symbol": "^3.3.0",
|
|
38
38
|
"@quenty/table": "^3.6.0",
|
|
39
39
|
"@quenty/throttle": "^10.8.1"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "00e6f71716216dd6ecbc8505ad898a1ab7f72756"
|
|
45
45
|
}
|
|
@@ -107,7 +107,7 @@ function Observable.new(onSubscribe)
|
|
|
107
107
|
assert(type(onSubscribe) == "function", "Bad onSubscribe")
|
|
108
108
|
|
|
109
109
|
return setmetatable({
|
|
110
|
-
_source = if ENABLE_STACK_TRACING then debug.traceback() else nil;
|
|
110
|
+
_source = if ENABLE_STACK_TRACING then debug.traceback("Observable.new()", 2) else nil;
|
|
111
111
|
_onSubscribe = onSubscribe;
|
|
112
112
|
}, Observable)
|
|
113
113
|
end
|
|
@@ -36,12 +36,28 @@ function ObservableSubscriptionTable:Fire(key, ...)
|
|
|
36
36
|
-- Make a copy so we don't have to worry about our last changing
|
|
37
37
|
for _, sub in pairs(table.clone(subs)) do
|
|
38
38
|
if sub:IsPending() then
|
|
39
|
+
-- TODO: Use connection here
|
|
39
40
|
task.spawn(sub.Fire, sub, ...)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
--[=[
|
|
46
|
+
Returns true if subscription exists
|
|
47
|
+
|
|
48
|
+
@param key TKey
|
|
49
|
+
@return boolean
|
|
50
|
+
]=]
|
|
51
|
+
function ObservableSubscriptionTable:HasSubscriptions(key)
|
|
52
|
+
return self._subMap[key] ~= nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
--[=[
|
|
56
|
+
Completes the subscription
|
|
57
|
+
|
|
58
|
+
@param key TKey
|
|
59
|
+
]=]
|
|
60
|
+
function ObservableSubscriptionTable:Complete(key)
|
|
45
61
|
local subs = self._subMap[key]
|
|
46
62
|
if not subs then
|
|
47
63
|
return
|
|
@@ -52,7 +68,28 @@ function ObservableSubscriptionTable:Complete(key, ...)
|
|
|
52
68
|
|
|
53
69
|
for _, sub in pairs(subsToComplete) do
|
|
54
70
|
if sub:IsPending() then
|
|
55
|
-
task.spawn(sub.Complete, sub
|
|
71
|
+
task.spawn(sub.Complete, sub)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
--[=[
|
|
77
|
+
Fails the subscription
|
|
78
|
+
|
|
79
|
+
@param key TKey
|
|
80
|
+
]=]
|
|
81
|
+
function ObservableSubscriptionTable:Fail(key)
|
|
82
|
+
local subs = self._subMap[key]
|
|
83
|
+
if not subs then
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
local subsToFail = table.clone(subs)
|
|
88
|
+
self._subMap[key] = nil
|
|
89
|
+
|
|
90
|
+
for _, sub in pairs(subsToFail) do
|
|
91
|
+
if sub:IsPending() then
|
|
92
|
+
task.spawn(sub.Fail, sub)
|
|
56
93
|
end
|
|
57
94
|
end
|
|
58
95
|
end
|
|
@@ -83,6 +120,7 @@ function ObservableSubscriptionTable:Observe(key, retrieveInitialValue)
|
|
|
83
120
|
return
|
|
84
121
|
end
|
|
85
122
|
|
|
123
|
+
-- TODO: Linked list
|
|
86
124
|
local index = table.find(current, sub)
|
|
87
125
|
if not index then
|
|
88
126
|
return
|
|
@@ -95,9 +133,7 @@ function ObservableSubscriptionTable:Observe(key, retrieveInitialValue)
|
|
|
95
133
|
|
|
96
134
|
-- Complete the subscription
|
|
97
135
|
if sub:IsPending() then
|
|
98
|
-
task.spawn(
|
|
99
|
-
sub:Complete()
|
|
100
|
-
end)
|
|
136
|
+
task.spawn(sub.Complete, sub)
|
|
101
137
|
end
|
|
102
138
|
end
|
|
103
139
|
end)
|
|
@@ -113,9 +149,7 @@ function ObservableSubscriptionTable:Destroy()
|
|
|
113
149
|
|
|
114
150
|
for _, sub in pairs(list) do
|
|
115
151
|
if sub:IsPending() then
|
|
116
|
-
task.spawn(
|
|
117
|
-
sub:Complete()
|
|
118
|
-
end)
|
|
152
|
+
task.spawn(sub.Complete, sub)
|
|
119
153
|
end
|
|
120
154
|
end
|
|
121
155
|
end
|
package/src/Shared/Rx.lua
CHANGED
|
@@ -19,6 +19,7 @@ local Symbol = require("Symbol")
|
|
|
19
19
|
local ThrottledFunction = require("ThrottledFunction")
|
|
20
20
|
local cancellableDelay = require("cancellableDelay")
|
|
21
21
|
local CancelToken = require("CancelToken")
|
|
22
|
+
local MaidTaskUtils = require("MaidTaskUtils")
|
|
22
23
|
|
|
23
24
|
local UNSET_VALUE = Symbol.named("unsetValue")
|
|
24
25
|
|
|
@@ -138,6 +139,8 @@ end
|
|
|
138
139
|
@return Promise<T>
|
|
139
140
|
]=]
|
|
140
141
|
function Rx.toPromise(observable, cancelToken)
|
|
142
|
+
assert(Observable.isObservable(observable), "Bad observable")
|
|
143
|
+
|
|
141
144
|
local maid = Maid.new()
|
|
142
145
|
|
|
143
146
|
local newCancelToken = CancelToken.new(function(cancel)
|
|
@@ -1008,7 +1011,7 @@ function Rx.switchAll()
|
|
|
1008
1011
|
if currentInside == observable then
|
|
1009
1012
|
sub:Fire(...)
|
|
1010
1013
|
else
|
|
1011
|
-
warn(string.format("[Rx.switchAll] - Observable is still firing despite disconnect (%q)", observable._source))
|
|
1014
|
+
warn(string.format("[Rx.switchAll] - Observable is still firing despite disconnect (%q)", tostring(observable._source)))
|
|
1012
1015
|
end
|
|
1013
1016
|
end, -- Merge each inner observable
|
|
1014
1017
|
function(...)
|
|
@@ -1446,12 +1449,14 @@ function Rx.combineLatest(observables)
|
|
|
1446
1449
|
|
|
1447
1450
|
return Observable.new(function(sub)
|
|
1448
1451
|
local pending = 0
|
|
1452
|
+
local unset = 0
|
|
1449
1453
|
local latest = {}
|
|
1450
1454
|
|
|
1451
1455
|
-- Instead of caching this, use extra compute here
|
|
1452
1456
|
for key, value in pairs(observables) do
|
|
1453
1457
|
if Observable.isObservable(value) then
|
|
1454
|
-
pending
|
|
1458
|
+
pending += 1
|
|
1459
|
+
unset += 1
|
|
1455
1460
|
latest[key] = UNSET_VALUE
|
|
1456
1461
|
else
|
|
1457
1462
|
latest[key] = value
|
|
@@ -1466,44 +1471,130 @@ function Rx.combineLatest(observables)
|
|
|
1466
1471
|
|
|
1467
1472
|
local maid = Maid.new()
|
|
1468
1473
|
|
|
1469
|
-
local function
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
+
local function failOnFirst(...)
|
|
1475
|
+
pending -= 1
|
|
1476
|
+
latest = nil
|
|
1477
|
+
sub:Fail(...)
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1480
|
+
local function completeOnAllPendingDone()
|
|
1481
|
+
pending -= 1
|
|
1482
|
+
if pending == 0 then
|
|
1483
|
+
latest = nil
|
|
1484
|
+
sub:Complete()
|
|
1485
|
+
end
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
for key, observer in pairs(observables) do
|
|
1489
|
+
if not Observable.isObservable(observer) then
|
|
1490
|
+
continue
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
maid:GiveTask(observer:Subscribe(
|
|
1494
|
+
function(value)
|
|
1495
|
+
if latest[key] == UNSET_VALUE then
|
|
1496
|
+
unset -= 1
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
latest[key] = value
|
|
1500
|
+
|
|
1501
|
+
if unset == 0 then
|
|
1502
|
+
sub:Fire(table.freeze(table.clone(latest)))
|
|
1503
|
+
end
|
|
1504
|
+
end,
|
|
1505
|
+
failOnFirst,
|
|
1506
|
+
completeOnAllPendingDone))
|
|
1507
|
+
end
|
|
1508
|
+
|
|
1509
|
+
return maid
|
|
1510
|
+
end)
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
--[=[
|
|
1514
|
+
Equivalent of [Rx.combineLatest] and [Rx.throttleDefer] but avoids copying and emitting a new table
|
|
1515
|
+
until after the frame ends. Helpful in scenarios where we write multiple times to a single value in a
|
|
1516
|
+
frame, and we don't want to create a lot of work for the garbage collector.
|
|
1517
|
+
|
|
1518
|
+
@param observables { [TKey]: Observable<TEmitted> | TEmitted }
|
|
1519
|
+
@return Observable<{ [TKey]: TEmitted }>
|
|
1520
|
+
]=]
|
|
1521
|
+
function Rx.combineLatestDefer(observables)
|
|
1522
|
+
assert(type(observables) == "table", "Bad observables")
|
|
1523
|
+
|
|
1524
|
+
return Observable.new(function(sub)
|
|
1525
|
+
local pending = 0
|
|
1526
|
+
local unset = 0
|
|
1527
|
+
local latest = {}
|
|
1528
|
+
|
|
1529
|
+
-- Instead of caching this, use extra compute here
|
|
1530
|
+
for key, value in pairs(observables) do
|
|
1531
|
+
if Observable.isObservable(value) then
|
|
1532
|
+
pending += 1
|
|
1533
|
+
unset += 1
|
|
1534
|
+
latest[key] = UNSET_VALUE
|
|
1535
|
+
else
|
|
1536
|
+
latest[key] = value
|
|
1474
1537
|
end
|
|
1538
|
+
end
|
|
1475
1539
|
|
|
1476
|
-
|
|
1540
|
+
if pending == 0 then
|
|
1541
|
+
sub:Fire(latest)
|
|
1542
|
+
sub:Complete()
|
|
1543
|
+
return
|
|
1477
1544
|
end
|
|
1478
1545
|
|
|
1546
|
+
local maid = Maid.new()
|
|
1547
|
+
|
|
1479
1548
|
local function failOnFirst(...)
|
|
1480
|
-
pending
|
|
1549
|
+
pending -= 1
|
|
1550
|
+
latest = nil
|
|
1481
1551
|
sub:Fail(...)
|
|
1482
1552
|
end
|
|
1483
1553
|
|
|
1484
1554
|
local function completeOnAllPendingDone()
|
|
1485
|
-
pending
|
|
1555
|
+
pending -= 1
|
|
1486
1556
|
if pending == 0 then
|
|
1557
|
+
latest = nil
|
|
1487
1558
|
sub:Complete()
|
|
1488
1559
|
end
|
|
1489
1560
|
end
|
|
1490
1561
|
|
|
1562
|
+
local queueThread = nil
|
|
1563
|
+
maid:GiveTask(function()
|
|
1564
|
+
if queueThread then
|
|
1565
|
+
MaidTaskUtils.doTask(queueThread)
|
|
1566
|
+
end
|
|
1567
|
+
end)
|
|
1568
|
+
|
|
1491
1569
|
for key, observer in pairs(observables) do
|
|
1492
|
-
if Observable.isObservable(observer) then
|
|
1493
|
-
|
|
1494
|
-
function(value)
|
|
1495
|
-
latest[key] = value
|
|
1496
|
-
fireIfAllSet()
|
|
1497
|
-
end,
|
|
1498
|
-
failOnFirst,
|
|
1499
|
-
completeOnAllPendingDone))
|
|
1570
|
+
if not Observable.isObservable(observer) then
|
|
1571
|
+
continue
|
|
1500
1572
|
end
|
|
1573
|
+
|
|
1574
|
+
maid:GiveTask(observer:Subscribe(
|
|
1575
|
+
function(value)
|
|
1576
|
+
if latest[key] == UNSET_VALUE then
|
|
1577
|
+
unset -= 1
|
|
1578
|
+
end
|
|
1579
|
+
|
|
1580
|
+
latest[key] = value
|
|
1581
|
+
|
|
1582
|
+
if unset == 0 and not queueThread then
|
|
1583
|
+
queueThread = task.defer(function()
|
|
1584
|
+
queueThread = nil
|
|
1585
|
+
sub:Fire(table.freeze(table.clone(latest)))
|
|
1586
|
+
end)
|
|
1587
|
+
end
|
|
1588
|
+
end,
|
|
1589
|
+
failOnFirst,
|
|
1590
|
+
completeOnAllPendingDone))
|
|
1501
1591
|
end
|
|
1502
1592
|
|
|
1503
1593
|
return maid
|
|
1504
1594
|
end)
|
|
1505
1595
|
end
|
|
1506
1596
|
|
|
1597
|
+
|
|
1507
1598
|
--[=[
|
|
1508
1599
|
http://reactivex.io/documentation/operators/using.html
|
|
1509
1600
|
|
|
@@ -47,7 +47,7 @@ function Subscription.new(fireCallback, failCallback, completeCallback, observab
|
|
|
47
47
|
|
|
48
48
|
return setmetatable({
|
|
49
49
|
_state = SubscriptionStateTypes.PENDING;
|
|
50
|
-
_source = if ENABLE_STACK_TRACING then debug.traceback() else nil;
|
|
50
|
+
_source = if ENABLE_STACK_TRACING then debug.traceback("Subscription.new()", 3) else nil;
|
|
51
51
|
_observableSource = observableSource;
|
|
52
52
|
_fireCallback = fireCallback;
|
|
53
53
|
_failCallback = failCallback;
|
|
@@ -66,12 +66,19 @@ function Subscription:Fire(...)
|
|
|
66
66
|
self._fireCallback(...)
|
|
67
67
|
end
|
|
68
68
|
elseif self._state == SubscriptionStateTypes.CANCELLED then
|
|
69
|
-
|
|
69
|
+
if self._fireCountAfterDeath then
|
|
70
|
+
self._fireCountAfterDeath += 1
|
|
71
|
+
else
|
|
72
|
+
self._fireCountAfterDeath = 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if self._fireCountAfterDeath > 1 then
|
|
76
|
+
warn(debug.traceback(string.format("Subscription:Fire(%s) called %d times after death. Be sure to disconnect all events.", tostring(...), self._fireCountAfterDeath), 2))
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
if ENABLE_STACK_TRACING then
|
|
79
|
+
print(self._observableSource)
|
|
80
|
+
print(self._source)
|
|
81
|
+
end
|
|
75
82
|
end
|
|
76
83
|
end
|
|
77
84
|
end
|
|
@@ -172,33 +179,35 @@ function Subscription:IsPending()
|
|
|
172
179
|
return self._state == SubscriptionStateTypes.PENDING
|
|
173
180
|
end
|
|
174
181
|
|
|
175
|
-
function Subscription:_assignCleanup(
|
|
182
|
+
function Subscription:_assignCleanup(cleanupTask)
|
|
176
183
|
assert(self._cleanupTask == nil, "Already have _cleanupTask")
|
|
177
184
|
|
|
178
|
-
if MaidTaskUtils.isValidTask(
|
|
185
|
+
if MaidTaskUtils.isValidTask(cleanupTask) then
|
|
179
186
|
if self._state ~= SubscriptionStateTypes.PENDING then
|
|
180
|
-
MaidTaskUtils.doTask(
|
|
187
|
+
MaidTaskUtils.doTask(cleanupTask)
|
|
181
188
|
return
|
|
182
189
|
end
|
|
183
190
|
|
|
184
|
-
self._cleanupTask =
|
|
185
|
-
elseif
|
|
186
|
-
error("Bad cleanup
|
|
191
|
+
self._cleanupTask = cleanupTask
|
|
192
|
+
elseif cleanupTask ~= nil then
|
|
193
|
+
error("Bad cleanup cleanupTask")
|
|
187
194
|
end
|
|
188
195
|
end
|
|
189
196
|
|
|
190
197
|
function Subscription:_doCleanup()
|
|
191
|
-
local
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
end
|
|
198
|
+
local cleanupTask = self._cleanupTask
|
|
199
|
+
if cleanupTask then
|
|
200
|
+
self._cleanupTask = nil
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
MaidTaskUtils.doTask(task)
|
|
202
|
+
-- The validity can change
|
|
203
|
+
if MaidTaskUtils.isValidTask(cleanupTask) then
|
|
204
|
+
MaidTaskUtils.doTask(cleanupTask)
|
|
205
|
+
end
|
|
201
206
|
end
|
|
207
|
+
|
|
208
|
+
self._fireCallback = nil
|
|
209
|
+
self._failCallback = nil
|
|
210
|
+
self._completeCallback = nil
|
|
202
211
|
end
|
|
203
212
|
|
|
204
213
|
--[=[
|
|
@@ -215,6 +224,7 @@ function Subscription:Destroy()
|
|
|
215
224
|
end
|
|
216
225
|
|
|
217
226
|
self:_doCleanup()
|
|
227
|
+
|
|
218
228
|
end
|
|
219
229
|
|
|
220
230
|
--[=[
|