mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-04-25 08:00:03 +00:00
Optimize Promise.delay with linked-list impl
- Always iterate over promises via `ipairs` - Avoid pushing the call arguments stack to a table in Promise.prototype.expect and Promise.prototype.await - Use a doubly-linked list implementation of a queue The old queue/dequeue implementation used an array which: - has items removed from the front (`table.remove(queue, 1)` O(n) each time) - this is especially bad in the main loop which could run multiple times in-a-row on a large array - new: O(1) - uses table.insert() followed by table.sort() to add a new node (O(n log n)) - new: O(n) - has to lookup the index of the node being dequeued (O(n)) - new: O(1)
This commit is contained in:
parent
64ba0099f1
commit
e1e183d632
1 changed files with 105 additions and 70 deletions
173
lib/init.lua
173
lib/init.lua
|
@ -16,18 +16,14 @@ local RunService = game:GetService("RunService")
|
||||||
Used to cajole varargs without dropping sparse values.
|
Used to cajole varargs without dropping sparse values.
|
||||||
]]
|
]]
|
||||||
local function pack(...)
|
local function pack(...)
|
||||||
local len = select("#", ...)
|
return select("#", ...), { ... }
|
||||||
|
|
||||||
return len, { ... }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Returns first value (success), and packs all following values.
|
Returns first value (success), and packs all following values.
|
||||||
]]
|
]]
|
||||||
local function packResult(...)
|
local function packResult(...)
|
||||||
local result = (...)
|
return ..., pack(select(2, ...))
|
||||||
|
|
||||||
return result, pack(select(2, ...))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -185,9 +181,10 @@ end
|
||||||
function Promise._newWithSelf(executor, ...)
|
function Promise._newWithSelf(executor, ...)
|
||||||
local args
|
local args
|
||||||
local promise = Promise.new(function(...)
|
local promise = Promise.new(function(...)
|
||||||
args = {...}
|
args = { ... }
|
||||||
end, ...)
|
end, ...)
|
||||||
|
|
||||||
|
-- we don't handle the length here since `args` will always be { resolve, reject, onCancelHook }
|
||||||
executor(promise, unpack(args))
|
executor(promise, unpack(args))
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
|
@ -196,7 +193,6 @@ end
|
||||||
function Promise._new(traceback, executor, ...)
|
function Promise._new(traceback, executor, ...)
|
||||||
return Promise._newWithSelf(function(self, ...)
|
return Promise._newWithSelf(function(self, ...)
|
||||||
self._source = traceback
|
self._source = traceback
|
||||||
|
|
||||||
executor(...)
|
executor(...)
|
||||||
end, ...)
|
end, ...)
|
||||||
end
|
end
|
||||||
|
@ -262,7 +258,7 @@ function Promise._all(traceback, promises, amount)
|
||||||
|
|
||||||
-- We need to check that each value is a promise here so that we can produce
|
-- We need to check that each value is a promise here so that we can produce
|
||||||
-- a proper error rather than a rejected promise with our error.
|
-- a proper error rather than a rejected promise with our error.
|
||||||
for i, promise in pairs(promises) do
|
for i, promise in ipairs(promises) do
|
||||||
if not Promise.is(promise) then
|
if not Promise.is(promise) then
|
||||||
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 3)
|
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.all", tostring(i)), 3)
|
||||||
end
|
end
|
||||||
|
@ -317,10 +313,10 @@ function Promise._all(traceback, promises, amount)
|
||||||
|
|
||||||
-- We can assume the values inside `promises` are all promises since we
|
-- We can assume the values inside `promises` are all promises since we
|
||||||
-- checked above.
|
-- checked above.
|
||||||
for i = 1, #promises do
|
for i, promise in ipairs(promises) do
|
||||||
table.insert(
|
table.insert(
|
||||||
newPromises,
|
newPromises,
|
||||||
promises[i]:andThen(
|
promise:andThen(
|
||||||
function(...)
|
function(...)
|
||||||
resolveOne(i, ...)
|
resolveOne(i, ...)
|
||||||
end,
|
end,
|
||||||
|
@ -367,7 +363,7 @@ function Promise.allSettled(promises)
|
||||||
|
|
||||||
-- We need to check that each value is a promise here so that we can produce
|
-- We need to check that each value is a promise here so that we can produce
|
||||||
-- a proper error rather than a rejected promise with our error.
|
-- a proper error rather than a rejected promise with our error.
|
||||||
for i, promise in pairs(promises) do
|
for i, promise in ipairs(promises) do
|
||||||
if not Promise.is(promise) then
|
if not Promise.is(promise) then
|
||||||
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.allSettled", tostring(i)), 2)
|
error((ERROR_NON_PROMISE_IN_LIST):format("Promise.allSettled", tostring(i)), 2)
|
||||||
end
|
end
|
||||||
|
@ -406,10 +402,10 @@ function Promise.allSettled(promises)
|
||||||
|
|
||||||
-- We can assume the values inside `promises` are all promises since we
|
-- We can assume the values inside `promises` are all promises since we
|
||||||
-- checked above.
|
-- checked above.
|
||||||
for i = 1, #promises do
|
for i, promise in ipairs(promises) do
|
||||||
table.insert(
|
table.insert(
|
||||||
newPromises,
|
newPromises,
|
||||||
promises[i]:finally(
|
promise:finally(
|
||||||
function(...)
|
function(...)
|
||||||
resolveOne(i, ...)
|
resolveOne(i, ...)
|
||||||
end
|
end
|
||||||
|
@ -426,7 +422,7 @@ end
|
||||||
function Promise.race(promises)
|
function Promise.race(promises)
|
||||||
assert(type(promises) == "table", ERROR_NON_LIST:format("Promise.race"))
|
assert(type(promises) == "table", ERROR_NON_LIST:format("Promise.race"))
|
||||||
|
|
||||||
for i, promise in pairs(promises) do
|
for i, promise in ipairs(promises) do
|
||||||
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -500,58 +496,96 @@ end
|
||||||
Creates a Promise that resolves after given number of seconds.
|
Creates a Promise that resolves after given number of seconds.
|
||||||
]]
|
]]
|
||||||
do
|
do
|
||||||
|
-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert
|
||||||
|
|
||||||
|
-- the initial node in the linked list
|
||||||
|
local first
|
||||||
local connection
|
local connection
|
||||||
local queue = {}
|
|
||||||
|
|
||||||
local function enqueue(callback, seconds)
|
|
||||||
table.insert(queue, {
|
|
||||||
callback = callback,
|
|
||||||
startTime = tick(),
|
|
||||||
endTime = tick() + math.max(seconds, 1/60)
|
|
||||||
})
|
|
||||||
|
|
||||||
table.sort(queue, function(a, b)
|
|
||||||
return a.endTime < b.endTime
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not connection then
|
|
||||||
connection = RunService.Heartbeat:Connect(function()
|
|
||||||
while #queue > 0 and queue[1].endTime <= tick() do
|
|
||||||
local item = table.remove(queue, 1)
|
|
||||||
|
|
||||||
item.callback(tick() - item.startTime)
|
|
||||||
end
|
|
||||||
|
|
||||||
if #queue == 0 then
|
|
||||||
connection:Disconnect()
|
|
||||||
connection = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dequeue(callback)
|
|
||||||
for i, item in ipairs(queue) do
|
|
||||||
if item.callback == callback then
|
|
||||||
table.remove(queue, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Promise.delay(seconds)
|
function Promise.delay(seconds)
|
||||||
assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
|
assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
|
||||||
-- If seconds is -INF, INF, or NaN, assume seconds is 0.
|
-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
|
||||||
-- This mirrors the behavior of wait()
|
-- This mirrors the behavior of wait()
|
||||||
if seconds < 0 or seconds == math.huge or seconds ~= seconds then
|
if not (seconds >= 1 / 60) or seconds == math.huge then
|
||||||
seconds = 0
|
seconds = 1 / 60
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
||||||
enqueue(resolve, seconds)
|
local startTime = tick()
|
||||||
|
local endTime = startTime + seconds
|
||||||
|
|
||||||
|
local node = {
|
||||||
|
resolve = resolve,
|
||||||
|
startTime = startTime,
|
||||||
|
endTime = endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if connection == nil then -- first is nil when connection is nil
|
||||||
|
first = node
|
||||||
|
connection = RunService.Heartbeat:Connect(function()
|
||||||
|
local currentTime = tick()
|
||||||
|
|
||||||
|
while first.endTime <= currentTime do
|
||||||
|
first.resolve(currentTime - first.startTime)
|
||||||
|
first = first.next
|
||||||
|
if first == nil then
|
||||||
|
connection:Disconnect()
|
||||||
|
connection = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
first.previous = nil
|
||||||
|
currentTime = tick()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else -- first is non-nil
|
||||||
|
if first.endTime < endTime then -- if `node` should be placed after `first`
|
||||||
|
-- we will insert `node` between `current` and `next`
|
||||||
|
-- (i.e. after `current` if `next` is nil)
|
||||||
|
local current = first
|
||||||
|
local next = current.next
|
||||||
|
|
||||||
|
while next ~= nil and next.endTime < endTime do
|
||||||
|
current = next
|
||||||
|
next = current.next
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `current` must be non-nil, but `next` could be `nil` (i.e. last item in list)
|
||||||
|
current.next = node
|
||||||
|
node.previous = current
|
||||||
|
|
||||||
|
if next ~= nil then
|
||||||
|
node.next = next
|
||||||
|
next.previous = node
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- set `node` to `first`
|
||||||
|
node.next = first
|
||||||
|
first.previous = node
|
||||||
|
first = node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
onCancel(function()
|
onCancel(function()
|
||||||
dequeue(resolve)
|
-- remove node from queue
|
||||||
|
local next = node.next
|
||||||
|
|
||||||
|
if first == node then
|
||||||
|
if next == nil then -- if `node` is the first and last
|
||||||
|
connection:Disconnect()
|
||||||
|
connection = nil
|
||||||
|
else -- if `node` is `first` and not the last
|
||||||
|
next.previous = nil
|
||||||
|
end
|
||||||
|
first = next
|
||||||
|
else
|
||||||
|
local previous = node.previous
|
||||||
|
-- since `node` is not `first`, then we know `previous` is non-nil
|
||||||
|
previous.next = next
|
||||||
|
|
||||||
|
if next ~= nil then
|
||||||
|
next.previous = previous
|
||||||
|
end
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -862,14 +896,23 @@ function Promise.prototype:awaitStatus()
|
||||||
return self._status
|
return self._status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function awaitHelper(status, ...)
|
||||||
|
return status == Promise.Status.Resolved, ...
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Calls awaitStatus internally, returns (isResolved, values...)
|
Calls awaitStatus internally, returns (isResolved, values...)
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:await(...)
|
function Promise.prototype:await(...)
|
||||||
local length, result = pack(self:awaitStatus(...))
|
return awaitHelper(self:awaitStatus(...))
|
||||||
local status = table.remove(result, 1)
|
end
|
||||||
|
|
||||||
return status == Promise.Status.Resolved, unpack(result, 1, length - 1)
|
local function expectHelper(status, ...)
|
||||||
|
if status ~= Promise.Status.Resolved then
|
||||||
|
error((...) == nil and "" or tostring((...)), 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ...
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -877,15 +920,7 @@ end
|
||||||
Throws if the Promise rejects or gets cancelled.
|
Throws if the Promise rejects or gets cancelled.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:expect(...)
|
function Promise.prototype:expect(...)
|
||||||
local length, result = pack(self:awaitStatus(...))
|
return expectHelper(self:awaitStatus(...))
|
||||||
local status = table.remove(result, 1)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
status == Promise.Status.Resolved,
|
|
||||||
tostring(result[1] == nil and "" or result[1])
|
|
||||||
)
|
|
||||||
|
|
||||||
return unpack(result, 1, length - 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Promise.prototype.awaitValue = Promise.prototype.expect
|
Promise.prototype.awaitValue = Promise.prototype.expect
|
||||||
|
|
Loading…
Reference in a new issue