You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
11 KiB
339 lines
11 KiB
--========================================================--
|
|
-- Scorpio Reactive Extension --
|
|
-- --
|
|
-- Author : kurapica125@outlook.com --
|
|
-- Create Date : 2019/12/03 --
|
|
--========================================================--
|
|
|
|
--========================================================--
|
|
Scorpio "Scorpio.Reactive" ""
|
|
--========================================================--
|
|
|
|
local _M = _M
|
|
|
|
import "System.Reactive"
|
|
|
|
------------------------------------------------------------
|
|
-- Time Operation --
|
|
------------------------------------------------------------
|
|
do
|
|
--- Create an Observable that emits a sequence of integers spaced by a given time interval
|
|
__Static__() __Arguments__{ Number, Number/nil }
|
|
function Observable.Interval(interval, max)
|
|
return Observable(function(observer)
|
|
return Continue(
|
|
function (observer, interval, max)
|
|
local i = 0
|
|
max = max or math.huge
|
|
|
|
while not observer.IsUnsubscribed and i <= max do
|
|
observer:OnNext(i)
|
|
Delay(interval)
|
|
i = i + 1
|
|
end
|
|
|
|
observer:OnCompleted()
|
|
end,
|
|
observer, interval, max
|
|
)
|
|
end)
|
|
end
|
|
|
|
--- Creates an Observable that emits a particular item after a given delay
|
|
__Static__() __Arguments__{ Number }
|
|
function Observable.Timer(delay)
|
|
return Observable(function(observer)
|
|
return Delay(delay,
|
|
function (observer)
|
|
observer:OnNext(0)
|
|
observer:OnCompleted()
|
|
end,
|
|
observer
|
|
)
|
|
end)
|
|
end
|
|
|
|
--- The Delay extension method is a purely a way to time-shift an entire sequence
|
|
__Observable__()
|
|
__Arguments__{ Number }
|
|
function IObservable:Delay(delay)
|
|
return Operator(self, function(observer, ...)
|
|
return Delay(delay,
|
|
function (observer, ...)
|
|
observer:OnNext(...)
|
|
end,
|
|
observer, ...
|
|
)
|
|
end, nil, function(observer)
|
|
return Delay(delay,
|
|
function (observer)
|
|
observer:OnCompleted()
|
|
end,
|
|
observer
|
|
)
|
|
end)
|
|
end
|
|
|
|
--- The Timeout extension method allows us terminate the sequence with an error if
|
|
-- we do not receive any notifications for a given period
|
|
--
|
|
-- Usage: Observable.Range(1, 10):Delay(2):Timeout(1):Dump()
|
|
__Observable__()
|
|
__Arguments__{ Number }
|
|
function IObservable:Timeout(dueTime)
|
|
return Observable(function(observer)
|
|
local count = 0
|
|
local check = function(chkcnt)
|
|
return chkcnt == count and observer:OnError("The operation is time out")
|
|
end
|
|
|
|
Delay(dueTime, check, count)
|
|
|
|
self:Subscribe(function(...)
|
|
count = count + 1
|
|
observer:OnNext(...)
|
|
Delay(dueTime, check, count)
|
|
end, function(ex)
|
|
observer:OnError(ex)
|
|
end, function()
|
|
observer:OnCompleted()
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Block the sequence for a frame phase, useful for some events that trigger
|
|
-- multiple-times in one phase(the same value will be blocked until the next phase)
|
|
local _Recyle = Recycle()
|
|
local fakefunc = Toolset.fakefunc
|
|
|
|
local function onNextSingleProcess(observer, cache, idxmap)
|
|
for i = 1, #cache do
|
|
local single = cache[i]
|
|
if idxmap[single] == i then
|
|
if single == fakefunc then
|
|
observer:OnNext(nil)
|
|
else
|
|
observer:OnNext(single)
|
|
end
|
|
end
|
|
end
|
|
|
|
_Recyle(wipe(cache))
|
|
_Recyle(wipe(idxmap))
|
|
end
|
|
|
|
local function onNextProcess(observer, cache, idxmap)
|
|
for i = 1, #cache do
|
|
local item = cache[i]
|
|
if idxmap[item[0]] == i then
|
|
observer:OnNext(unpack(item, 1))
|
|
wipe(item)
|
|
end
|
|
end
|
|
|
|
_Recyle(wipe(cache))
|
|
_Recyle(wipe(idxmap))
|
|
end
|
|
|
|
local function distinctCache(cache, idxmap, ...)
|
|
local item = _Recyle()
|
|
local ncnt = select("#", ...)
|
|
local index = 1
|
|
|
|
for i = 1, ncnt do
|
|
item[i] = tostring((select(i, ...)))
|
|
end
|
|
|
|
local token = tblconcat(item, "|")
|
|
local index = #cache + 1
|
|
|
|
if idxmap[token] then
|
|
cache[index] = cache[idxmap[token]]
|
|
_Recyle(wipe(item))
|
|
else
|
|
item[0] = token
|
|
for i = 1, ncnt do
|
|
item[i] = select(i, ...)
|
|
end
|
|
|
|
cache[index] = item
|
|
end
|
|
idxmap[token] = index
|
|
end
|
|
|
|
__Observable__()
|
|
function IObservable:Next(multi)
|
|
local cache, idxmap
|
|
local currTime = 0
|
|
|
|
if multi then
|
|
return Operator(self, function(observer, ...)
|
|
local now = GetTime()
|
|
if now ~= currTime then
|
|
cache = _Recyle()
|
|
idxmap = _Recyle()
|
|
currTime = now
|
|
Next(onNextProcess, observer, cache, idxmap)
|
|
end
|
|
distinctCache(cache, idxmap, ...)
|
|
end)
|
|
else
|
|
return Operator(self, function(observer, single)
|
|
local now = GetTime()
|
|
if now ~= currTime then
|
|
cache = _Recyle()
|
|
idxmap = _Recyle()
|
|
currTime = now
|
|
Next(onNextSingleProcess, observer, cache, idxmap)
|
|
end
|
|
if single == nil then single = fakefunc end
|
|
local idx = #cache + 1
|
|
cache[idx] = single
|
|
idxmap[single] = idx
|
|
end)
|
|
end
|
|
end
|
|
|
|
--- Filter the unit event with unit, this should be re-usable
|
|
__Arguments__{ String }
|
|
function IObservable:MatchUnit(unit)
|
|
local matchUnits = self.__MatchUnits
|
|
if not matchUnits then
|
|
matchUnits = {}
|
|
self.__MatchUnits = matchUnits
|
|
|
|
self:Subscribe(function(unit, ...)
|
|
if unit == "any" then
|
|
for nunit, subject in pairs(matchUnits) do
|
|
subject:OnNext(nunit, ...)
|
|
end
|
|
else
|
|
local subject = matchUnits[unit]
|
|
return subject and subject:OnNext(unit, ...)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local subject = matchUnits[unit]
|
|
if not subject then
|
|
subject = Subject()
|
|
matchUnits[unit] = subject
|
|
end
|
|
|
|
return subject
|
|
end
|
|
|
|
if not IObservable.Throttle then
|
|
__Observable__()
|
|
__Arguments__{ Number }
|
|
function IObservable:Throttle(dueTime)
|
|
local lasttime = 0
|
|
|
|
return Operator(self, function(observer, ...)
|
|
local curr = GetTime()
|
|
if curr - lasttime > dueTime then
|
|
lasttime = curr
|
|
observer:OnNext(...)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
local DebounceTask = Toolset.newtable(true)
|
|
local DebounceCache = Recycle()
|
|
|
|
__Service__(true)
|
|
function DebounceService()
|
|
while true do
|
|
local hasTasks = false
|
|
local curr = GetTime()
|
|
|
|
for ob, task in pairs(DebounceTask) do
|
|
hasTasks = true
|
|
|
|
if task.lasttime <= curr then
|
|
DebounceTask[ob] = nil
|
|
ob:OnNext(unpack(task))
|
|
|
|
DebounceCache(wipe(task))
|
|
end
|
|
end
|
|
|
|
if not hasTasks then NextEvent("SCORPIO_DEBOUNCE_SERVICE_START") end
|
|
|
|
Delay(0.1)
|
|
end
|
|
end
|
|
|
|
__Observable__()
|
|
__Arguments__{ Number }
|
|
function IObservable:Debounce(dueTime)
|
|
if dueTime <= 0 then dueTime = 1 end
|
|
|
|
return Operator(self, function(observer, ...)
|
|
local cache = DebounceTask[observer] or DebounceCache()
|
|
cache.lasttime = GetTime() + dueTime
|
|
local n = select("#", ...)
|
|
local cn = #cache
|
|
|
|
if n <= 5 and cn <= 5 then
|
|
cache[1], cache[2], cache[3], cache[4], cache[5] = ...
|
|
else
|
|
for i = 1, n > cn and n or cn do
|
|
cache[i] = select(i, ...)
|
|
end
|
|
end
|
|
|
|
if not next(DebounceTask) then FireSystemEvent("SCORPIO_DEBOUNCE_SERVICE_START") end
|
|
DebounceTask[observer] = cache
|
|
end)
|
|
end
|
|
|
|
function IObservable:ColorString()
|
|
return self:Map(function(color)
|
|
if type(color) == "table" then
|
|
return strformat("\124c%.2x%.2x%.2x%.2x", (color.a or 1) * 255, (color.r or 1) * 255, (color.g or 1) * 255, (color.b or 1) * 255)
|
|
else
|
|
return ""
|
|
end
|
|
end):ToSubject(LiteralSubject)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------
|
|
-- Wow Observable --
|
|
------------------------------------------------------------
|
|
__Final__() __Sealed__()
|
|
interface "Scorpio.Wow" (function(_ENV)
|
|
local _EventMap = setmetatable({}, {
|
|
__index = function(self, event)
|
|
local subject = Subject()
|
|
rawset(self, event, subject)
|
|
|
|
-- Keep register since if the event is used, it should be used frequently
|
|
_M:RegisterEvent(event, function(...) subject:OnNext(...) end)
|
|
|
|
return subject
|
|
end
|
|
})
|
|
|
|
local _MultiEventMap = {}
|
|
|
|
--- The data sequences from the wow event
|
|
__Static__() __Arguments__{ NEString * 1 }
|
|
function FromEvent(...)
|
|
if select("#", ...) == 1 then
|
|
return _EventMap[(...)]
|
|
else
|
|
local token = List{ ... }:Join("|")
|
|
local ob = _MultiEventMap[token]
|
|
if not ob then
|
|
ob = Observable(function(observer) for i = 1, #ob do _EventMap[ob[i]]:Subscribe(observer) end end)
|
|
for i = 1, select("#", ...) do ob[i] = select(i, ...) end
|
|
_MultiEventMap[token] = ob
|
|
end
|
|
|
|
return ob
|
|
end
|
|
end
|
|
end)
|
|
|