@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 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.11.1",
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.8.1",
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.7.1",
36
- "@quenty/signal": "^7.8.1",
37
- "@quenty/symbol": "^3.2.0",
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": "01c43a0ddd3c5e0cb2d9027313dbfa9852eedef1"
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
- function ObservableSubscriptionTable:Complete(key, ...)
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(function()
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(function()
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 = pending + 1
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 fireIfAllSet()
1470
- for _, value in pairs(latest) do
1471
- if value == UNSET_VALUE then
1472
- return
1473
- end
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
- sub:Fire(table.freeze(table.clone(latest)))
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 = pending - 1
1549
+ pending -= 1
1550
+ latest = nil
1481
1551
  sub:Fail(...)
1482
1552
  end
1483
1553
 
1484
1554
  local function completeOnAllPendingDone()
1485
- pending = pending - 1
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
- maid:GiveTask(observer:Subscribe(
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
- warn("[Subscription.Fire] - We are cancelled, but events are still being pushed")
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
- if ENABLE_STACK_TRACING then
72
- print(debug.traceback())
73
- print(self._source)
74
- print(self._observableSource)
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(task)
182
+ function Subscription:_assignCleanup(cleanupTask)
176
183
  assert(self._cleanupTask == nil, "Already have _cleanupTask")
177
184
 
178
- if MaidTaskUtils.isValidTask(task) then
185
+ if MaidTaskUtils.isValidTask(cleanupTask) then
179
186
  if self._state ~= SubscriptionStateTypes.PENDING then
180
- MaidTaskUtils.doTask(task)
187
+ MaidTaskUtils.doTask(cleanupTask)
181
188
  return
182
189
  end
183
190
 
184
- self._cleanupTask = task
185
- elseif task ~= nil then
186
- error("Bad cleanup task")
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 task = self._cleanupTask
192
- if not task then
193
- return
194
- end
198
+ local cleanupTask = self._cleanupTask
199
+ if cleanupTask then
200
+ self._cleanupTask = nil
195
201
 
196
- self._cleanupTask = nil
197
-
198
- -- The validity can change
199
- if MaidTaskUtils.isValidTask(task) then
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
  --[=[