@quenty/brio 14.29.2 → 14.30.1

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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "targets": {
3
+ "test": {
4
+ "universeId": 9716264427,
5
+ "placeId": 122679562100430,
6
+ "project": "test/default.project.json",
7
+ "scriptTemplate": "test/scripts/Server/ServerMain.server.lua"
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/brio",
3
- "version": "14.29.2",
3
+ "version": "14.30.1",
4
4
  "description": "Brios wrap an object and either are alive or dead",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -32,8 +32,8 @@
32
32
  "@quenty/loader": "10.11.0",
33
33
  "@quenty/maid": "3.9.0",
34
34
  "@quenty/nevermore-test-runner": "1.4.0",
35
- "@quenty/rx": "13.28.2",
36
- "@quenty/signal": "7.13.0",
35
+ "@quenty/rx": "13.28.3",
36
+ "@quenty/signal": "7.13.1",
37
37
  "@quenty/steputils": "3.6.3",
38
38
  "@quenty/table": "3.9.2",
39
39
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
@@ -41,5 +41,5 @@
41
41
  "publishConfig": {
42
42
  "access": "public"
43
43
  },
44
- "gitHead": "7672b52f13af6a10df1b189d19d6e2404b5b3e55"
44
+ "gitHead": "598b2b62b36bdcbdbbd56f7db10c399831cc6eba"
45
45
  }
@@ -21,7 +21,7 @@
21
21
 
22
22
  Brios are useful for downstream events where you want to emit a resource. Typically
23
23
  brios should be killed when their source is killed. Brios are intended to be merged
24
- with downstream brios so create a chain of reliable resources.
24
+ with downstream brios to create a chain of reliable resources.
25
25
 
26
26
  ```lua
27
27
  local brio = Brio.new("a", "b")
@@ -103,7 +103,7 @@ function Brio.new<T...>(...: T...): Brio<T...>
103
103
  end
104
104
 
105
105
  --[=[
106
- Constructs a new brio that will cleanup afer the set amount of time
106
+ Constructs a new brio that will cleanup after the set amount of time
107
107
 
108
108
  @since 3.6.0
109
109
  @param time number
@@ -153,7 +153,7 @@ function Brio.GetDiedSignal<T...>(self: Brio<T...>): Signal.Signal<T...>
153
153
  end
154
154
 
155
155
  --[=[
156
- Returns true is the brio is dead.
156
+ Returns true if the brio is dead.
157
157
 
158
158
  ```lua
159
159
  local brio = Brio.new("a", "b")
@@ -287,7 +287,7 @@ function Brio.Destroy<T...>(self: Brio<T...>)
287
287
  end
288
288
 
289
289
  --[=[
290
- Alias for Destroy.
290
+ Alias for [Brio.Destroy].
291
291
  @method Kill
292
292
  @within Brio
293
293
  ]=]
@@ -471,7 +471,7 @@ end
471
471
  3. All resources are invalidated
472
472
  4. We still wanted to be able to use most of the resources
473
473
 
474
- With this method we are able to do this, as we'll re-emit a table with all resoruces
474
+ With this method we are able to do this, as we'll re-emit a table with all resources
475
475
  except the invalidated one.
476
476
 
477
477
  @since 3.6.0
@@ -10,6 +10,7 @@ local require = (require :: any)(
10
10
  local Brio = require("Brio")
11
11
  local Jest = require("Jest")
12
12
  local Observable = require("Observable")
13
+ local Rx = require("Rx")
13
14
  local RxBrioUtils = require("RxBrioUtils")
14
15
 
15
16
  local describe = Jest.Globals.describe
@@ -34,8 +35,8 @@ end)
34
35
  describe("RxBrioUtils.combineLatest({ value = Observable(Brio(5)) })", function()
35
36
  it("should execute immediately", function()
36
37
  local observe = RxBrioUtils.combineLatest({
37
- value = Observable.new(function(sub)
38
- sub:Fire(Brio.new(5))
38
+ value = Observable.new(function(innerSub)
39
+ innerSub:Fire(Brio.new(5))
39
40
  end),
40
41
  otherValue = 25,
41
42
  })
@@ -123,3 +124,444 @@ describe("RxBrioUtils.flatCombineLatest", function()
123
124
  sub:Destroy()
124
125
  end)
125
126
  end)
127
+
128
+ describe("RxBrioUtils.switchToBrio", function()
129
+ it("should wrap a plain value in a brio", function()
130
+ local result
131
+ local sub = Observable.new(function(innerSub)
132
+ innerSub:Fire(42)
133
+ end)
134
+ :Pipe({
135
+ RxBrioUtils.switchToBrio(),
136
+ })
137
+ :Subscribe(function(brio)
138
+ result = brio
139
+ end)
140
+
141
+ expect(result).never.toBeNil()
142
+ expect(Brio.isBrio(result)).toEqual(true)
143
+ expect(result:IsDead()).toEqual(false)
144
+ expect(result:GetValue()).toEqual(42)
145
+
146
+ sub:Destroy()
147
+ end)
148
+
149
+ it("should wrap multiple plain values, packing them into a brio", function()
150
+ local result
151
+ local sub = Observable.new(function(innerSub)
152
+ innerSub:Fire("a", "b")
153
+ end)
154
+ :Pipe({
155
+ RxBrioUtils.switchToBrio(),
156
+ })
157
+ :Subscribe(function(brio)
158
+ result = brio
159
+ end)
160
+
161
+ expect(result).never.toBeNil()
162
+ expect(Brio.isBrio(result)).toEqual(true)
163
+ expect(result:IsDead()).toEqual(false)
164
+
165
+ local a, b = result:GetValue()
166
+ expect(a).toEqual("a")
167
+ expect(b).toEqual("b")
168
+
169
+ sub:Destroy()
170
+ end)
171
+
172
+ it("should clone an input brio instead of forwarding it directly", function()
173
+ local inputBrio = Brio.new(99)
174
+ local result
175
+ local sub = Observable.new(function(innerSub)
176
+ innerSub:Fire(inputBrio)
177
+ end)
178
+ :Pipe({
179
+ RxBrioUtils.switchToBrio(),
180
+ })
181
+ :Subscribe(function(brio)
182
+ result = brio
183
+ end)
184
+
185
+ expect(result).never.toBeNil()
186
+ expect(Brio.isBrio(result)).toEqual(true)
187
+ expect(result:GetValue()).toEqual(99)
188
+ -- Should be a clone, not the same object
189
+ expect(result).never.toEqual(inputBrio)
190
+
191
+ sub:Destroy()
192
+ inputBrio:Kill()
193
+ end)
194
+
195
+ it("should kill the previous brio when a new value is emitted", function()
196
+ local doFire
197
+ local results = {}
198
+ local sub = Observable.new(function(innerSub)
199
+ doFire = function(...)
200
+ innerSub:Fire(...)
201
+ end
202
+ end)
203
+ :Pipe({
204
+ RxBrioUtils.switchToBrio(),
205
+ })
206
+ :Subscribe(function(brio)
207
+ table.insert(results, brio)
208
+ end)
209
+
210
+ doFire(1)
211
+ doFire(2)
212
+
213
+ expect(#results).toEqual(2)
214
+ expect(results[1]:IsDead()).toEqual(true)
215
+ expect(results[2]:IsDead()).toEqual(false)
216
+ expect(results[2]:GetValue()).toEqual(2)
217
+
218
+ sub:Destroy()
219
+ end)
220
+
221
+ it("should kill the previous brio even when the new value is filtered by predicate", function()
222
+ local doFire
223
+ local results = {}
224
+ local sub = Observable.new(function(innerSub)
225
+ doFire = function(...)
226
+ innerSub:Fire(...)
227
+ end
228
+ end)
229
+ :Pipe({
230
+ RxBrioUtils.switchToBrio(function(value)
231
+ return value ~= "skip"
232
+ end),
233
+ })
234
+ :Subscribe(function(brio)
235
+ table.insert(results, brio)
236
+ end)
237
+
238
+ doFire("keep")
239
+ expect(#results).toEqual(1)
240
+ expect(results[1]:IsDead()).toEqual(false)
241
+
242
+ doFire("skip") -- predicate rejects, but should still kill previous
243
+ expect(#results).toEqual(1)
244
+ expect(results[1]:IsDead()).toEqual(true)
245
+
246
+ sub:Destroy()
247
+ end)
248
+
249
+ it("should ignore dead brios from the source", function()
250
+ local deadBrio = Brio.new(10)
251
+ deadBrio:Kill()
252
+
253
+ local result
254
+ local fireCount = 0
255
+ local sub = Rx.of(deadBrio)
256
+ :Pipe({
257
+ RxBrioUtils.switchToBrio(),
258
+ })
259
+ :Subscribe(function(brio)
260
+ result = brio
261
+ fireCount = fireCount + 1
262
+ end)
263
+
264
+ expect(fireCount).toEqual(0)
265
+ expect(result).toBeNil()
266
+
267
+ sub:Destroy()
268
+ end)
269
+
270
+ it("should kill clone when the source brio dies", function()
271
+ local inputBrio = Brio.new(5)
272
+ local result
273
+ local sub = Observable.new(function(innerSub)
274
+ innerSub:Fire(inputBrio)
275
+ end)
276
+ :Pipe({
277
+ RxBrioUtils.switchToBrio(),
278
+ })
279
+ :Subscribe(function(brio)
280
+ result = brio
281
+ end)
282
+
283
+ expect(result).never.toBeNil()
284
+ expect(result:IsDead()).toEqual(false)
285
+
286
+ inputBrio:Kill()
287
+ expect(result:IsDead()).toEqual(true)
288
+
289
+ sub:Destroy()
290
+ end)
291
+
292
+ it("should kill the last brio on unsubscribe", function()
293
+ local result
294
+ local sub = Observable.new(function(innerSub)
295
+ innerSub:Fire(77)
296
+ end)
297
+ :Pipe({
298
+ RxBrioUtils.switchToBrio(),
299
+ })
300
+ :Subscribe(function(brio)
301
+ result = brio
302
+ end)
303
+
304
+ expect(result).never.toBeNil()
305
+ expect(result:IsDead()).toEqual(false)
306
+
307
+ sub:Destroy()
308
+ expect(result:IsDead()).toEqual(true)
309
+ end)
310
+
311
+ it("should propagate failure from source", function()
312
+ local failed = false
313
+ local failMsg
314
+ local sub = Observable.new(function(innerSub)
315
+ innerSub:Fail("test error")
316
+ end)
317
+ :Pipe({
318
+ RxBrioUtils.switchToBrio(),
319
+ })
320
+ :Subscribe(function() end, function(err)
321
+ failed = true
322
+ failMsg = err
323
+ end)
324
+
325
+ expect(failed).toEqual(true)
326
+ expect(failMsg).toEqual("test error")
327
+
328
+ sub:Destroy()
329
+ end)
330
+
331
+ it("should propagate completion from source", function()
332
+ local completed = false
333
+ local sub = Observable.new(function(innerSub)
334
+ innerSub:Complete()
335
+ end)
336
+ :Pipe({
337
+ RxBrioUtils.switchToBrio(),
338
+ })
339
+ :Subscribe(function() end, function() end, function()
340
+ completed = true
341
+ end)
342
+
343
+ expect(completed).toEqual(true)
344
+
345
+ sub:Destroy()
346
+ end)
347
+
348
+ it("should apply predicate to plain values", function()
349
+ local doFire
350
+ local results = {}
351
+ local sub = Observable.new(function(innerSub)
352
+ doFire = function(...)
353
+ innerSub:Fire(...)
354
+ end
355
+ end)
356
+ :Pipe({
357
+ RxBrioUtils.switchToBrio(function(value)
358
+ return value > 10
359
+ end),
360
+ })
361
+ :Subscribe(function(brio)
362
+ table.insert(results, brio)
363
+ end)
364
+
365
+ doFire(5)
366
+ expect(#results).toEqual(0)
367
+
368
+ doFire(15)
369
+ expect(#results).toEqual(1)
370
+ expect(results[1]:GetValue()).toEqual(15)
371
+
372
+ doFire(3)
373
+ expect(#results).toEqual(1)
374
+ expect(results[1]:IsDead()).toEqual(true)
375
+
376
+ sub:Destroy()
377
+ end)
378
+
379
+ it("should apply predicate to unwrapped brio values", function()
380
+ local doFire
381
+ local results = {}
382
+ local sub = Observable.new(function(innerSub)
383
+ doFire = function(...)
384
+ innerSub:Fire(...)
385
+ end
386
+ end)
387
+ :Pipe({
388
+ RxBrioUtils.switchToBrio(function(value)
389
+ return value > 10
390
+ end),
391
+ })
392
+ :Subscribe(function(brio)
393
+ table.insert(results, brio)
394
+ end)
395
+
396
+ doFire(Brio.new(5))
397
+ expect(#results).toEqual(0)
398
+
399
+ doFire(Brio.new(20))
400
+ expect(#results).toEqual(1)
401
+ expect(results[1]:GetValue()).toEqual(20)
402
+
403
+ sub:Destroy()
404
+ end)
405
+
406
+ it("should handle rapid succession of emissions correctly", function()
407
+ local doFire
408
+ local results = {}
409
+ local sub = Observable.new(function(innerSub)
410
+ doFire = function(...)
411
+ innerSub:Fire(...)
412
+ end
413
+ end)
414
+ :Pipe({
415
+ RxBrioUtils.switchToBrio(),
416
+ })
417
+ :Subscribe(function(brio)
418
+ table.insert(results, brio)
419
+ end)
420
+
421
+ for i = 1, 100 do
422
+ doFire(i)
423
+ end
424
+
425
+ expect(#results).toEqual(100)
426
+
427
+ -- All but last should be dead
428
+ for i = 1, 99 do
429
+ expect(results[i]:IsDead()).toEqual(true)
430
+ end
431
+ expect(results[100]:IsDead()).toEqual(false)
432
+ expect(results[100]:GetValue()).toEqual(100)
433
+
434
+ sub:Destroy()
435
+ end)
436
+
437
+ it("should handle interleaved brio and plain value emissions", function()
438
+ local doFire
439
+ local results = {}
440
+ local sub = Observable.new(function(innerSub)
441
+ doFire = function(...)
442
+ innerSub:Fire(...)
443
+ end
444
+ end)
445
+ :Pipe({
446
+ RxBrioUtils.switchToBrio(),
447
+ })
448
+ :Subscribe(function(brio)
449
+ table.insert(results, brio)
450
+ end)
451
+
452
+ local inputBrio = Brio.new("from-brio")
453
+ doFire("plain")
454
+ doFire(inputBrio)
455
+ doFire("plain-again")
456
+
457
+ expect(#results).toEqual(3)
458
+ expect(results[1]:IsDead()).toEqual(true)
459
+ expect(results[2]:IsDead()).toEqual(true)
460
+ expect(results[3]:IsDead()).toEqual(false)
461
+ expect(results[3]:GetValue()).toEqual("plain-again")
462
+
463
+ sub:Destroy()
464
+ end)
465
+
466
+ it("should handle source brio dying while subscribed then new emission", function()
467
+ local doFire
468
+ local results = {}
469
+ local sub = Observable.new(function(innerSub)
470
+ doFire = function(...)
471
+ innerSub:Fire(...)
472
+ end
473
+ end)
474
+ :Pipe({
475
+ RxBrioUtils.switchToBrio(),
476
+ })
477
+ :Subscribe(function(brio)
478
+ table.insert(results, brio)
479
+ end)
480
+
481
+ local brio1 = Brio.new("first")
482
+ doFire(brio1)
483
+ expect(#results).toEqual(1)
484
+ expect(results[1]:IsDead()).toEqual(false)
485
+
486
+ -- Source brio dies externally
487
+ brio1:Kill()
488
+ expect(results[1]:IsDead()).toEqual(true)
489
+
490
+ -- New emission should still work
491
+ doFire("second")
492
+ expect(#results).toEqual(2)
493
+ expect(results[2]:IsDead()).toEqual(false)
494
+ expect(results[2]:GetValue()).toEqual("second")
495
+
496
+ sub:Destroy()
497
+ end)
498
+
499
+ it("should handle subscriber causing re-emission during fire", function()
500
+ -- Race condition: subscriber fires a new value during the callback
501
+ local doFire
502
+ local results = {}
503
+ local reEmitted = false
504
+
505
+ local sub = Observable.new(function(innerSub)
506
+ doFire = function(...)
507
+ innerSub:Fire(...)
508
+ end
509
+ end)
510
+ :Pipe({
511
+ RxBrioUtils.switchToBrio(),
512
+ })
513
+ :Subscribe(function(brio)
514
+ table.insert(results, brio)
515
+
516
+ -- On first emission, synchronously emit another value
517
+ if not reEmitted then
518
+ reEmitted = true
519
+ doFire("re-emitted")
520
+ end
521
+ end)
522
+
523
+ doFire("initial")
524
+
525
+ expect(#results).toEqual(2)
526
+ -- The initial brio should be dead because re-emission killed it
527
+ expect(results[1]:IsDead()).toEqual(true)
528
+ -- The re-emitted brio should be alive
529
+ expect(results[2]:IsDead()).toEqual(false)
530
+ expect(results[2]:GetValue()).toEqual("re-emitted")
531
+
532
+ sub:Destroy()
533
+ end)
534
+
535
+ it("should not emit when predicate is provided and all values are rejected", function()
536
+ local doFire
537
+ local fireCount = 0
538
+
539
+ local sub = Observable.new(function(innerSub)
540
+ doFire = function(...)
541
+ innerSub:Fire(...)
542
+ end
543
+ end)
544
+ :Pipe({
545
+ RxBrioUtils.switchToBrio(function()
546
+ return false
547
+ end),
548
+ })
549
+ :Subscribe(function()
550
+ fireCount = fireCount + 1
551
+ end)
552
+
553
+ doFire(1)
554
+ doFire(2)
555
+ doFire(Brio.new(3))
556
+
557
+ expect(fireCount).toEqual(0)
558
+
559
+ sub:Destroy()
560
+ end)
561
+
562
+ it("should error with non-function predicate", function()
563
+ expect(function()
564
+ RxBrioUtils.switchToBrio("bad" :: any)
565
+ end).toThrow()
566
+ end)
567
+ end)
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "BrioTest",
3
+ "globIgnorePaths": [
4
+ "**/.package-lock.json",
5
+ "**/.pnpm",
6
+ "**/.pnpm-workspace-state-v1.json",
7
+ "**/.modules.yaml",
8
+ "**/.ignored",
9
+ "**/.ignored_*"
10
+ ],
11
+ "tree": {
12
+ "$className": "DataModel",
13
+ "ServerScriptService": {
14
+ "$properties": {
15
+ "LoadStringEnabled": true
16
+ },
17
+ "brio": {
18
+ "$path": ".."
19
+ },
20
+ "Script": {
21
+ "$path": "scripts/Server"
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ --[[
2
+ @class ServerMain
3
+ ]]
4
+ local ServerScriptService = game:GetService("ServerScriptService")
5
+
6
+ local root = ServerScriptService.brio
7
+ local loader = root:FindFirstChild("LoaderUtils", true).Parent
8
+ local require = require(loader).bootstrapGame(root)
9
+
10
+ local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
11
+ if NevermoreTestRunnerUtils.runTestsIfNeededAsync(root) then
12
+ return
13
+ end