@quenty/promise 10.21.0 → 10.22.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 +6 -0
- package/package.json +2 -2
- package/src/Shared/Promise.lua +12 -3
- package/src/Shared/Promise.spec.lua +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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
|
+
# [10.22.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/promise@10.21.0...@quenty/promise@10.22.0) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Even more stuff ([fea7e95](https://github.com/Quenty/NevermoreEngine/commit/fea7e9587e7195bbfa7f4753c130a3d597f9a34b))
|
|
11
|
+
|
|
6
12
|
# [10.21.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/promise@10.20.0...@quenty/promise@10.21.0) (2026-07-23)
|
|
7
13
|
|
|
8
14
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/promise",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.22.0",
|
|
4
4
|
"description": "Promise implementation for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "88b03743718172ff79cd62ade64a86565fcac17d"
|
|
43
43
|
}
|
package/src/Shared/Promise.lua
CHANGED
|
@@ -433,15 +433,24 @@ function Promise.Then<T...>(
|
|
|
433
433
|
onFulfilled: ((T...) -> ...any)?,
|
|
434
434
|
onRejected: ((...any) -> ...any)?
|
|
435
435
|
): Promise<...any>
|
|
436
|
-
if
|
|
436
|
+
if self._pendingExecuteList then
|
|
437
|
+
-- Attaching any continuation delegates this promise's rejection to the returned
|
|
438
|
+
-- child promise, which becomes the new carrier of the unconsumed exception. So the
|
|
439
|
+
-- parent is consumed even without an onRejected here: a rejection flowing through a
|
|
440
|
+
-- handler-less Then is forwarded, not dropped, and the warning surfaces only at the
|
|
441
|
+
-- tail of the chain if nobody ever handles it.
|
|
437
442
|
self._unconsumedException = false
|
|
438
|
-
end
|
|
439
443
|
|
|
440
|
-
if self._pendingExecuteList then
|
|
441
444
|
local promise: Promise<T...> = Promise.new()
|
|
442
445
|
self._pendingExecuteList[#self._pendingExecuteList + 1] = { onFulfilled, onRejected, promise } :: { any }
|
|
443
446
|
return promise
|
|
444
447
|
else
|
|
448
|
+
-- Already settled: _executeThen returns self when there is no matching handler, so
|
|
449
|
+
-- self stays the carrier. Only an actual onRejected consumes the rejection here.
|
|
450
|
+
if type(onRejected) == "function" then
|
|
451
|
+
self._unconsumedException = false
|
|
452
|
+
end
|
|
453
|
+
|
|
445
454
|
return self:_executeThen(onFulfilled, onRejected, nil)
|
|
446
455
|
end
|
|
447
456
|
end
|
|
@@ -324,6 +324,123 @@ describe("Promise:Then", function()
|
|
|
324
324
|
end)
|
|
325
325
|
end)
|
|
326
326
|
|
|
327
|
+
describe("Promise:Then unconsumed-exception propagation", function()
|
|
328
|
+
it("consumes a pending promise once any continuation is attached", function()
|
|
329
|
+
local root = Promise.new()
|
|
330
|
+
expect((root :: any)._unconsumedException).toEqual(true)
|
|
331
|
+
|
|
332
|
+
root:Then(function() end)
|
|
333
|
+
|
|
334
|
+
expect((root :: any)._unconsumedException).toEqual(false)
|
|
335
|
+
end)
|
|
336
|
+
|
|
337
|
+
it("transfers the unconsumed exception to the returned child", function()
|
|
338
|
+
local root = Promise.new()
|
|
339
|
+
local child = root:Then(function() end)
|
|
340
|
+
|
|
341
|
+
expect((root :: any)._unconsumedException).toEqual(false)
|
|
342
|
+
expect((child :: any)._unconsumedException).toEqual(true)
|
|
343
|
+
|
|
344
|
+
root:Reject("boom")
|
|
345
|
+
|
|
346
|
+
-- Consume the child before the deferred uncaught-exception warning fires.
|
|
347
|
+
child:Catch(function() end)
|
|
348
|
+
end)
|
|
349
|
+
|
|
350
|
+
it("propagates a rejection through handler-less Thens to a downstream handler", function()
|
|
351
|
+
local root = Promise.new()
|
|
352
|
+
local tail = root:Then(function() end):Then(function() end):Then(nil, function(err)
|
|
353
|
+
return "recovered from " .. err
|
|
354
|
+
end)
|
|
355
|
+
|
|
356
|
+
root:Reject("boom")
|
|
357
|
+
|
|
358
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(tail)
|
|
359
|
+
expect(outcome).toEqual("resolved")
|
|
360
|
+
expect(value).toEqual("recovered from boom")
|
|
361
|
+
end)
|
|
362
|
+
|
|
363
|
+
it("consumes every intermediate hop so none of them warn", function()
|
|
364
|
+
local root = Promise.new()
|
|
365
|
+
local hop1 = root:Then(function() end)
|
|
366
|
+
local hop2 = hop1:Then(function() end)
|
|
367
|
+
hop2:Then(nil, function(err)
|
|
368
|
+
return err
|
|
369
|
+
end)
|
|
370
|
+
|
|
371
|
+
root:Reject("boom")
|
|
372
|
+
|
|
373
|
+
expect((root :: any)._unconsumedException).toEqual(false)
|
|
374
|
+
expect((hop1 :: any)._unconsumedException).toEqual(false)
|
|
375
|
+
expect((hop2 :: any)._unconsumedException).toEqual(false)
|
|
376
|
+
end)
|
|
377
|
+
|
|
378
|
+
it("leaves an unhandled tail flagged so the error is not silently dropped", function()
|
|
379
|
+
local root = Promise.new()
|
|
380
|
+
local tail = root:Then(function() end):Then(function() end)
|
|
381
|
+
|
|
382
|
+
root:Reject("boom")
|
|
383
|
+
|
|
384
|
+
expect(tail:IsRejected()).toEqual(true)
|
|
385
|
+
expect((tail :: any)._unconsumedException).toEqual(true)
|
|
386
|
+
|
|
387
|
+
-- Consume it before the deferred uncaught-exception warning fires.
|
|
388
|
+
tail:Catch(function() end)
|
|
389
|
+
expect((tail :: any)._unconsumedException).toEqual(false)
|
|
390
|
+
end)
|
|
391
|
+
|
|
392
|
+
it("gives each fan-out branch its own unconsumed carrier", function()
|
|
393
|
+
local root = Promise.new()
|
|
394
|
+
local branchA = root:Then(function() end)
|
|
395
|
+
local branchB = root:Then(function() end)
|
|
396
|
+
|
|
397
|
+
root:Reject("boom")
|
|
398
|
+
|
|
399
|
+
expect((branchA :: any)._unconsumedException).toEqual(true)
|
|
400
|
+
expect((branchB :: any)._unconsumedException).toEqual(true)
|
|
401
|
+
|
|
402
|
+
branchA:Catch(function() end)
|
|
403
|
+
branchB:Catch(function() end)
|
|
404
|
+
end)
|
|
405
|
+
|
|
406
|
+
it("does not consume an already-rejected source when onRejected is omitted", function()
|
|
407
|
+
local source = Promise.rejected("nope")
|
|
408
|
+
-- Settled + no onRejected returns self, so self stays the carrier and must remain flagged.
|
|
409
|
+
local same = source:Then(function() end)
|
|
410
|
+
|
|
411
|
+
expect(same == source).toEqual(true)
|
|
412
|
+
expect((source :: any)._unconsumedException).toEqual(true)
|
|
413
|
+
|
|
414
|
+
source:Catch(function() end)
|
|
415
|
+
expect((source :: any)._unconsumedException).toEqual(false)
|
|
416
|
+
end)
|
|
417
|
+
|
|
418
|
+
it("runs the tail error handler for the datastore graceful-close shape", function()
|
|
419
|
+
local outer = Promise.new()
|
|
420
|
+
local root = Promise.new()
|
|
421
|
+
|
|
422
|
+
root:Then(function()
|
|
423
|
+
return "unused"
|
|
424
|
+
end)
|
|
425
|
+
:Then(function()
|
|
426
|
+
return "unused"
|
|
427
|
+
end)
|
|
428
|
+
:Then(function(value)
|
|
429
|
+
outer:Resolve(value)
|
|
430
|
+
end, function(err)
|
|
431
|
+
outer:Reject(string.format("locked: %s", err))
|
|
432
|
+
end)
|
|
433
|
+
|
|
434
|
+
root:Reject("still locked")
|
|
435
|
+
|
|
436
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(outer)
|
|
437
|
+
expect(outcome).toEqual("rejected")
|
|
438
|
+
expect(err).toEqual("locked: still locked")
|
|
439
|
+
|
|
440
|
+
expect((root :: any)._unconsumedException).toEqual(false)
|
|
441
|
+
end)
|
|
442
|
+
end)
|
|
443
|
+
|
|
327
444
|
describe("Promise:Catch", function()
|
|
328
445
|
it("handles a rejection", function()
|
|
329
446
|
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.rejected("caught"):Catch(function(err)
|