add gs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local AppTempCommon = CorePackages.AppTempCommon
|
||||
local Promise = require(AppTempCommon.LuaApp.Promise)
|
||||
local ApiFetchThumbnails = require(AppTempCommon.LuaApp.Utils.ApiFetchThumbnails)
|
||||
local GamesGetIcons = require(AppTempCommon.LuaApp.Http.Requests.GamesGetIcons)
|
||||
local SetGameIcons = require(AppTempCommon.LuaApp.Actions.SetGameIcons)
|
||||
|
||||
local DEFAULT_ICON_SIZE = "150x150"
|
||||
|
||||
return function (networkImpl, universeIds, imageSize)
|
||||
return function(store)
|
||||
local state = store:getState()
|
||||
local stateToCheckForDuplicates = state.GameIcons
|
||||
|
||||
-- Filter out the icons that are already in the store.
|
||||
local idsToGet = {}
|
||||
for _, targetId in pairs(universeIds) do
|
||||
if stateToCheckForDuplicates[targetId] == nil then
|
||||
table.insert(idsToGet, targetId)
|
||||
end
|
||||
end
|
||||
|
||||
if #idsToGet == 0 then
|
||||
return Promise.resolve()
|
||||
else
|
||||
return ApiFetchThumbnails.Fetch(networkImpl,
|
||||
idsToGet, imageSize or DEFAULT_ICON_SIZE, "Game", GamesGetIcons, SetGameIcons, store)
|
||||
end
|
||||
end
|
||||
end
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local LuaApp = CorePackages.AppTempCommon.LuaApp
|
||||
|
||||
local GamesGetThumbnails = require(LuaApp.Http.Requests.GamesGetThumbnails)
|
||||
local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails)
|
||||
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local Promise = require(LuaApp.Promise)
|
||||
local Result = require(LuaApp.Result)
|
||||
|
||||
local TableUtilities = require(LuaApp.TableUtilities)
|
||||
|
||||
local THUMBNAIL_PAGE_COUNT = 20
|
||||
local THUMBNAIL_SIZE = 150
|
||||
local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries"))
|
||||
local RETRY_TIME_MULTIPLIER = 2 -- seconds
|
||||
|
||||
local function convertToId(value)
|
||||
if type(value) ~= "number" and type(value) ~= "string" then
|
||||
return Result.error("convertToId expects value passed in to be a number or a string")
|
||||
end
|
||||
|
||||
return Result.success(tostring(value))
|
||||
end
|
||||
|
||||
local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit)
|
||||
local someTokens = {}
|
||||
for i = 1, #thumbnailTokens, tokenLimit do
|
||||
local subArray = Functional.Take(thumbnailTokens, tokenLimit, i)
|
||||
table.insert(someTokens, subArray)
|
||||
end
|
||||
|
||||
return someTokens
|
||||
end
|
||||
|
||||
local function fetchThumbnailBatch(networkImpl, store, thumbnailTokens)
|
||||
return GamesGetThumbnails(networkImpl, thumbnailTokens, THUMBNAIL_SIZE, THUMBNAIL_SIZE):andThen(function(result)
|
||||
local thumbnails = {}
|
||||
local unfinalizedThumbnails = {}
|
||||
|
||||
for _,image in pairs(result.responseBody) do
|
||||
local convertToIdResult = convertToId(image.universeId)
|
||||
|
||||
convertToIdResult:match(function(universeId)
|
||||
if image.final == false then
|
||||
unfinalizedThumbnails[universeId] = image.retryToken
|
||||
else
|
||||
-- index all of the thumbnails by universeId
|
||||
thumbnails[universeId] = image
|
||||
end
|
||||
end, function(convertToIdError)
|
||||
warn(convertToIdError)
|
||||
end)
|
||||
end
|
||||
|
||||
store:dispatch(SetGameThumbnails(thumbnails))
|
||||
return Promise.resolve(unfinalizedThumbnails)
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetchSubdividedThumbnailsArray(networkImpl, store, thumbnailTokens)
|
||||
return fetchThumbnailBatch(networkImpl, store, thumbnailTokens):andThen(function(unfinalizedThumbnails)
|
||||
local remainingUnfinalizedThumbnails = unfinalizedThumbnails
|
||||
|
||||
for retryCount = 1, RETRY_MAX_COUNT do
|
||||
if TableUtilities.FieldCount(remainingUnfinalizedThumbnails) == 0 then
|
||||
return -- Bail out, we're done!
|
||||
end
|
||||
|
||||
wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1))
|
||||
remainingUnfinalizedThumbnails = fetchThumbnailBatch(networkImpl, store, remainingUnfinalizedThumbnails):await()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetchThumbnails(networkImpl, thumbnailTokens)
|
||||
return function(store)
|
||||
-- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time.
|
||||
-- So break apart the array of tokens we get into smaller, more manageable pieces.
|
||||
local fetchPromises = {}
|
||||
local someTokens = subdivideThumbnailTokenArray(thumbnailTokens, THUMBNAIL_PAGE_COUNT)
|
||||
for _, thumbsArr in ipairs(someTokens) do
|
||||
local promise = fetchSubdividedThumbnailsArray(networkImpl, store, thumbsArr)
|
||||
table.insert(fetchPromises, promise)
|
||||
end
|
||||
|
||||
return Promise.all(fetchPromises)
|
||||
end
|
||||
end
|
||||
|
||||
return fetchThumbnails
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local ApiFetchGameIcons = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameIcons)
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local GamesMultigetPlaceDetails = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GamesMultigetPlaceDetails)
|
||||
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
|
||||
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
|
||||
|
||||
local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags
|
||||
local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString)
|
||||
|
||||
return function(networkImpl, placeIds)
|
||||
return function(store)
|
||||
if not placeIds or #placeIds == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
return GamesMultigetPlaceDetails(networkImpl, placeIds):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
local thumbnailUniverseIds = {}
|
||||
local placeInfos = Functional.Map(data, function(placeInfoData)
|
||||
local placeInfo = PlaceInfoModel.fromWeb(placeInfoData)
|
||||
local universeId = convertUniverseIdToString(placeInfo.universeId)
|
||||
table.insert(thumbnailUniverseIds, universeId)
|
||||
return placeInfo
|
||||
end)
|
||||
|
||||
store:dispatch(ReceivedPlacesInfos(placeInfos))
|
||||
|
||||
if #thumbnailUniverseIds > 0 then
|
||||
store:dispatch(ApiFetchGameIcons(networkImpl, thumbnailUniverseIds))
|
||||
end
|
||||
|
||||
return placeInfos
|
||||
end)
|
||||
end
|
||||
end
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos)
|
||||
|
||||
-- LuaChat
|
||||
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
|
||||
local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos)
|
||||
|
||||
return function(networkImpl, placeIds)
|
||||
return function(store)
|
||||
return GetPlaceInfos(networkImpl, placeIds):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
local placeInfos = Functional.Map(data, function(placeInfoData)
|
||||
return PlaceInfoModel.fromWeb(placeInfoData)
|
||||
end)
|
||||
|
||||
store:dispatch(ReceivedMultiplePlaceInfos(placeInfos))
|
||||
|
||||
return placeInfos
|
||||
end)
|
||||
end
|
||||
end
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local UsersGetFriendCount = require(Requests.UsersGetFriendCount)
|
||||
local SetFriendCount = require(Actions.SetFriendCount)
|
||||
|
||||
local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled)
|
||||
|
||||
return function(networkImpl)
|
||||
return function(store)
|
||||
return UsersGetFriendCount(networkImpl):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
if isNewFriendsEndpointsEnabled() then
|
||||
if data.count then
|
||||
store:dispatch(SetFriendCount(data.count))
|
||||
end
|
||||
else
|
||||
if data.success and data.count then
|
||||
store:dispatch(SetFriendCount(data.count))
|
||||
end
|
||||
end
|
||||
|
||||
return data.count
|
||||
end)
|
||||
end
|
||||
end
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences)
|
||||
local ApiFetchUsersThumbnail = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersThumbnail)
|
||||
local UsersGetFriends = require(Requests.UsersGetFriends)
|
||||
|
||||
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
|
||||
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
|
||||
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
|
||||
local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
|
||||
|
||||
return function(requestImpl, userId, thumbnailRequest, checkPoints)
|
||||
return function(store)
|
||||
store:dispatch(FetchUserFriendsStarted(userId))
|
||||
|
||||
if checkPoints ~= nil and checkPoints.startFetchUserFriends ~= nil then
|
||||
checkPoints:startFetchUserFriends()
|
||||
end
|
||||
|
||||
local fetchedUserIds = {}
|
||||
return UsersGetFriends(requestImpl, userId):andThen(function(response)
|
||||
local responseBody = response.responseBody
|
||||
|
||||
local newUsers = {}
|
||||
for _, userData in pairs(responseBody.data) do
|
||||
local id = tostring(userData.id)
|
||||
|
||||
userData.isFriend = true
|
||||
local newUser = UserModel.fromDataTable(userData)
|
||||
|
||||
table.insert(fetchedUserIds, id)
|
||||
newUsers[newUser.id] = newUser
|
||||
end
|
||||
store:dispatch(UpdateUsers(newUsers))
|
||||
|
||||
if checkPoints ~= nil and checkPoints.finishFetchUserFriends ~= nil then
|
||||
checkPoints:finishFetchUserFriends()
|
||||
end
|
||||
|
||||
return fetchedUserIds
|
||||
end):andThen(function(userIds)
|
||||
if checkPoints ~= nil and checkPoints.startFetchUsersPresences ~= nil then
|
||||
checkPoints:startFetchUsersPresences()
|
||||
end
|
||||
-- Asynchronously fetch friend thumbnails so we don't block display of UI
|
||||
store:dispatch(ApiFetchUsersThumbnail.Fetch(requestImpl, userIds, thumbnailRequest))
|
||||
|
||||
return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds))
|
||||
end):andThen(
|
||||
function(result)
|
||||
store:dispatch(FetchUserFriendsCompleted(userId))
|
||||
|
||||
if checkPoints ~= nil and checkPoints.finishFetchUsersPresences ~= nil then
|
||||
checkPoints:finishFetchUsersPresences()
|
||||
end
|
||||
|
||||
return Promise.resolve(fetchedUserIds)
|
||||
end,
|
||||
function(response)
|
||||
store:dispatch(FetchUserFriendsFailed(userId, response))
|
||||
return Promise.reject(response)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local LuaApp = CorePackages.AppTempCommon.LuaApp
|
||||
local ChatUtils = CorePackages.AppTempCommon.LuaChat.Utils
|
||||
|
||||
local getPlaceIds = require(ChatUtils.getFriendsActiveGamesPlaceIdsFromUsersPresence)
|
||||
local receiveUsersPresence = require(ChatUtils.receiveUsersPresence)
|
||||
|
||||
local ApiFetchGamesDataByPlaceIds = require(LuaApp.Thunks.ApiFetchGamesDataByPlaceIds)
|
||||
local UsersGetPresence = require(LuaApp.Http.Requests.UsersGetPresence)
|
||||
|
||||
return function(networkImpl, userIds)
|
||||
return function(store)
|
||||
return UsersGetPresence(networkImpl, userIds):andThen(function(result)
|
||||
local userPresences = result.responseBody.userPresences
|
||||
receiveUsersPresence(userPresences, store)
|
||||
|
||||
local placeIds = getPlaceIds(userPresences, store)
|
||||
store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds))
|
||||
end)
|
||||
end
|
||||
end
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Cryo = require(CorePackages.Cryo)
|
||||
|
||||
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
|
||||
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
|
||||
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
|
||||
|
||||
local ThumbnailsGetAvatar = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatar)
|
||||
local ThumbnailsGetAvatarHeadshot = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatarHeadshot)
|
||||
|
||||
local AvatarThumbnailTypes = require(CorePackages.AppTempCommon.LuaApp.Enum.AvatarThumbnailTypes)
|
||||
|
||||
local SetUserThumbnail = require(Actions.SetUserThumbnail)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch)
|
||||
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
|
||||
|
||||
local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries"))
|
||||
local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier"))
|
||||
|
||||
local MAX_REQUEST_COUNT = 100
|
||||
|
||||
local ThumbnailsTypeToApiMap = {
|
||||
[AvatarThumbnailTypes.AvatarThumbnail] = ThumbnailsGetAvatar,
|
||||
[AvatarThumbnailTypes.HeadShot] = ThumbnailsGetAvatarHeadshot,
|
||||
}
|
||||
|
||||
local function subdivideEntries(entries, limit)
|
||||
local subArrays = {}
|
||||
for i = 1, #entries, limit do
|
||||
local subArray = Cryo.List.getRange(entries, i, i + limit - 1)
|
||||
table.insert(subArrays, subArray)
|
||||
end
|
||||
return subArrays
|
||||
end
|
||||
|
||||
local function keyMapper(userId, thumbnailType, thumbnailSize)
|
||||
return "luaapp.usersthumbnailsapi." .. userId .. "." .. thumbnailType .. "." .. thumbnailSize
|
||||
end
|
||||
|
||||
local function isCompleteThumbnailData(entry)
|
||||
return type(entry) == "table"
|
||||
and type(entry.targetId) == "number"
|
||||
and type(entry.state) == "string"
|
||||
and type(entry.imageUrl) == "string"
|
||||
end
|
||||
|
||||
local ApiFetchUsersThumbnail = {}
|
||||
|
||||
function ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize)
|
||||
assert(typeof(thumbnailSize) == "string",
|
||||
string.format("ApiFetchUsersThumbnail expects a string for thumbnailSize. Type: %s", typeof(thumbnailSize))
|
||||
)
|
||||
|
||||
assert(string.match(thumbnailSize, 'Size.+x'),
|
||||
string.format(
|
||||
"ApiFetchUsersThumbnail expects thumbnailSize to follow format \"Size..x..\" Current thumbnailSize: ",
|
||||
thumbnailSize
|
||||
)
|
||||
)
|
||||
return string.gsub(thumbnailSize, "Size", "")
|
||||
end
|
||||
|
||||
function ApiFetchUsersThumbnail._fetch(networkImpl, listOfUserIds, thumbnailRequest)
|
||||
local thumbnailSize = thumbnailRequest.thumbnailSize
|
||||
local thumbnailType = thumbnailRequest.thumbnailType
|
||||
|
||||
local thumbnailSizeRequestArg = ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize)
|
||||
local thumbnailsApiForThumbnailType = ThumbnailsTypeToApiMap[thumbnailType]
|
||||
|
||||
assert(typeof(thumbnailType) == "string",
|
||||
"ApiFetchUsersThumbnail expects thumbnailType to be a string")
|
||||
assert(typeof(thumbnailsApiForThumbnailType) == "function",
|
||||
"ApiFetchUsersThumbnail failed to find api for given type: ", thumbnailType)
|
||||
|
||||
local function keyMapperForCurrentTypeAndSize(userId)
|
||||
return keyMapper(userId, thumbnailType, thumbnailSize)
|
||||
end
|
||||
|
||||
local function getTableOfFailedResults(userIds)
|
||||
local results = {}
|
||||
for _, userId in pairs(userIds) do
|
||||
local key = keyMapperForCurrentTypeAndSize(userId)
|
||||
results[key] = Result.new(false, {
|
||||
targetId = userId,
|
||||
})
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
return PerformFetch.Batch(listOfUserIds, keyMapperForCurrentTypeAndSize, function(store, userIdsToFetch)
|
||||
local function fetchThumbnails(userIdsProvided)
|
||||
return thumbnailsApiForThumbnailType(networkImpl, userIdsProvided, thumbnailSizeRequestArg):andThen(
|
||||
function(result)
|
||||
local results = getTableOfFailedResults(userIdsProvided)
|
||||
local data = result and result.responseBody and result.responseBody.data
|
||||
if typeof(data) == "table" then
|
||||
for _, entry in pairs(data) do
|
||||
if isCompleteThumbnailData(entry) then
|
||||
local userId = tostring(entry.targetId)
|
||||
local key = keyMapperForCurrentTypeAndSize(userId)
|
||||
local success = false
|
||||
if entry.state == "Completed" then
|
||||
store:dispatch(SetUserThumbnail(tostring(entry.targetId), entry.imageUrl, thumbnailType, thumbnailSize))
|
||||
success = true
|
||||
end
|
||||
results[key] = Result.new(success, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Promise.resolve(results)
|
||||
end,
|
||||
function(err)
|
||||
local results = getTableOfFailedResults(userIdsProvided)
|
||||
return Promise.resolve(results)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return fetchThumbnails(userIdsToFetch):andThen(function(results)
|
||||
local completedThumbnails = {}
|
||||
local thumbnailResults = results
|
||||
|
||||
if _G.__TESTEZ_RUNNING_TEST__ then
|
||||
RETRY_MAX_COUNT = 1
|
||||
RETRY_TIME_MULTIPLIER = 0.001
|
||||
end
|
||||
|
||||
local function retry(retryCount)
|
||||
local remainingUserIdsToFetch = {}
|
||||
|
||||
for key, result in pairs(thumbnailResults) do
|
||||
local isSuccessful, thumbnailInfo = result:unwrap()
|
||||
|
||||
if isSuccessful and thumbnailInfo.state == "Completed" then
|
||||
completedThumbnails[key] = result
|
||||
elseif isSuccessful and thumbnailInfo.state == "Pending" then
|
||||
table.insert(remainingUserIdsToFetch, thumbnailInfo.targetId)
|
||||
end
|
||||
end
|
||||
|
||||
if TableUtilities.FieldCount(remainingUserIdsToFetch) == 0 then
|
||||
return Promise.resolve(completedThumbnails)
|
||||
end
|
||||
|
||||
local delayPromise = Promise.new(function(resolve, reject)
|
||||
coroutine.wrap(function()
|
||||
wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1))
|
||||
resolve()
|
||||
end)()
|
||||
end)
|
||||
|
||||
return delayPromise:andThen(function()
|
||||
return fetchThumbnails(remainingUserIdsToFetch)
|
||||
end):andThen(function(newResults)
|
||||
thumbnailResults = newResults
|
||||
if retryCount > 1 then
|
||||
return retry(retryCount - 1)
|
||||
else
|
||||
return Promise.resolve(completedThumbnails)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return retry(RETRY_MAX_COUNT)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiFetchUsersThumbnail.Fetch(networkImpl, userIds, thumbnailRequests)
|
||||
return function(store)
|
||||
local allPromises = {}
|
||||
local subArraysOfUserIds = subdivideEntries(userIds, MAX_REQUEST_COUNT)
|
||||
|
||||
for _, thumbnailRequest in pairs(thumbnailRequests) do
|
||||
for _, limitedListOfUserIds in pairs(subArraysOfUserIds) do
|
||||
local promise = store:dispatch(ApiFetchUsersThumbnail._fetch(networkImpl, limitedListOfUserIds, thumbnailRequest))
|
||||
table.insert(allPromises, promise)
|
||||
end
|
||||
end
|
||||
|
||||
return PromiseUtilities.Batch(allPromises)
|
||||
end
|
||||
end
|
||||
|
||||
function ApiFetchUsersThumbnail.GetFetchingStatus(state, userId, thumbnailType, thumbnailSize)
|
||||
return PerformFetch.GetStatus(state, keyMapper(userId, thumbnailType, thumbnailSize))
|
||||
end
|
||||
|
||||
return ApiFetchUsersThumbnail
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local ChatSendMessage = require(Requests.ChatSendMessage)
|
||||
local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation)
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
|
||||
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
|
||||
|
||||
|
||||
local ChatSendGameLinkMessage = require(Requests.ChatSendGameLinkMessage)
|
||||
|
||||
return function(networkImpl, userId, placeInfo)
|
||||
local clientId = Players.LocalPlayer.UserId
|
||||
|
||||
-- Construct the invite messages based on place info
|
||||
local inviteTextMessage
|
||||
inviteTextMessage = RobloxTranslator:FormatByKey(
|
||||
"Feature.SettingsHub.Message.InviteToGameTitle", { PLACENAME = placeInfo.name }
|
||||
)
|
||||
|
||||
return function(store)
|
||||
return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult)
|
||||
local conversation = conversationResult.responseBody.conversation
|
||||
|
||||
return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function()
|
||||
local function handleResult(inviteResult)
|
||||
local data = inviteResult.responseBody
|
||||
|
||||
return {
|
||||
resultType = data.resultType,
|
||||
conversationId = conversation.id,
|
||||
placeId = placeInfo.universeRootPlaceId,
|
||||
}
|
||||
end
|
||||
|
||||
return ChatSendGameLinkMessage(networkImpl, conversation.id, placeInfo.universeId):andThen(handleResult)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
|
||||
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
|
||||
|
||||
--[[
|
||||
PerformFetch wraps the notion of a network request together with its fetching status
|
||||
so that it is easier to de-duplicate concurrent requests for the same resource. The
|
||||
fetching status for individual fetching operations are available in the store as:
|
||||
|
||||
storeState.FetchingStatus[key]
|
||||
|
||||
When you use one of the methods in this helper, you provide a key (or keymap), and
|
||||
supply a functor that will only be called when a fetch actually needs to be performed.
|
||||
|
||||
Any follow-up andThen/catch clauses will be correctly daisy-chained onto the original
|
||||
ongoing fetch request if one is already underway.
|
||||
]]
|
||||
local PerformFetch = {}
|
||||
|
||||
local batchPromises = {} -- fetch key = outstanding promise from PerformFetch.Batch
|
||||
|
||||
--[[
|
||||
Helper function for unit tests to be able to clean up batchPromises created from
|
||||
previous test case. This is because unit tests don't wait until the mock requests
|
||||
are resolved and moves onto the next test. If tests happen to generate duplicate
|
||||
fetchStatusKey, unresolved batchPromise will throw thinking that the promise does
|
||||
not have the correct status.
|
||||
]]
|
||||
function PerformFetch.ClearOutstandingPromiseStatus()
|
||||
batchPromises = {}
|
||||
end
|
||||
|
||||
local function singleFetchKeymapper(item)
|
||||
-- Single fetch keys are used directly
|
||||
return item
|
||||
end
|
||||
|
||||
--[[
|
||||
Get the fetching status for a given status key. Defaults to
|
||||
RetrievalStatus.NotStarted for missing keys.
|
||||
]]
|
||||
function PerformFetch.GetStatus(state, fetchStatusKey)
|
||||
assert(typeof(state) == "table")
|
||||
assert(typeof(fetchStatusKey) == "string")
|
||||
assert(#fetchStatusKey > 0)
|
||||
return state.FetchingStatus[fetchStatusKey] or RetrievalStatus.NotStarted
|
||||
end
|
||||
|
||||
--[[
|
||||
Perform a fetch operation for a single resource.
|
||||
|
||||
Args:
|
||||
fetchStatusKey - String key for the fetching status to index the Rodux store.
|
||||
fetchFunctor - Functor to call when a fetch needs to be performed for fetchStatusKey.
|
||||
|
||||
Returns:
|
||||
A Promise that resolves or rejects in accordance with the result of fetchFunctor, or the
|
||||
promise for the original fetch if one is already ongoing.
|
||||
|
||||
Usage:
|
||||
In your main thunk, wrap your inner store function with this thunk, like this:
|
||||
|
||||
return function(arg1, arg2)
|
||||
return PerformFetch.Single("mykey", function(store)
|
||||
return doYourLogicHere() -- Must return a Promise!!!
|
||||
end)
|
||||
end
|
||||
|
||||
Please note that in order for single fetches to integrate well with batch fetches,
|
||||
your promise must NEVER resolve or reject with multiple arguments! Wrap your results
|
||||
in a table instead.
|
||||
]]
|
||||
function PerformFetch.Single(fetchStatusKey, fetchFunctor)
|
||||
assert(typeof(fetchStatusKey) == "string")
|
||||
assert(typeof(fetchFunctor) == "function")
|
||||
assert(#fetchStatusKey > 0)
|
||||
|
||||
return function(store)
|
||||
-- Call batch API to handle the individual fetch
|
||||
return PerformFetch.Batch({ fetchStatusKey }, singleFetchKeymapper, function(batchStore, itemsToFetch)
|
||||
assert(#itemsToFetch == 1)
|
||||
|
||||
local functorPromise = fetchFunctor(batchStore)
|
||||
assert(Promise.is(functorPromise))
|
||||
|
||||
return functorPromise:andThen(function(...)
|
||||
assert(#{...} <= 1)
|
||||
return Promise.resolve({ [fetchStatusKey] = Result.new(true, (...)) })
|
||||
end, function(...)
|
||||
assert(#{...} <= 1)
|
||||
return Promise.resolve({ [fetchStatusKey] = Result.new(false, (...)) })
|
||||
end)
|
||||
end)(store):andThen(function(batchResults)
|
||||
local success, value = batchResults[fetchStatusKey]:unwrap()
|
||||
if success then
|
||||
return Promise.resolve(value)
|
||||
else
|
||||
return Promise.reject(value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Perform a fetch operation for multiple resources at once (batching).
|
||||
|
||||
Args:
|
||||
items - The list of item ids that need to be fetched.
|
||||
keyMapper - A function that maps items to string keys for the Rodux store.
|
||||
fetchFunctor - A function that will be called when at least one item needs to be fetched.
|
||||
|
||||
Returns:
|
||||
A Promise that always resolves. Result data is returned in a single table to the
|
||||
andThen() clause, where item fetch keys are the keys and the results for each key
|
||||
are encoded using a Result object.
|
||||
|
||||
Usage:
|
||||
In your main thunk, wrap your inner store function with this thunk, like this:
|
||||
|
||||
local MAPPER = function(item)
|
||||
return doSomething(item) -- Each key must be unique
|
||||
end
|
||||
|
||||
return function(arg1, arg2)
|
||||
local allItems = makeYourItemsList()
|
||||
return PerformFetch.Batch(allItems, MAPPER, function(store, itemsToFetch)
|
||||
return doYourLogicHere(itemsToFetch) -- Must return a Promise!!!
|
||||
end)
|
||||
end
|
||||
|
||||
Your implementation of fetchFunctor should return a promise that resolves
|
||||
according to the structure of PromiseUtilities.Batch, ex:
|
||||
|
||||
return Promise.resolve({
|
||||
itemFetchKey1 = Result.new(true, payload1),
|
||||
itemFetchKey2 = Result.new(false, payload2), -- failed
|
||||
})
|
||||
|
||||
Any other resolving arguments will be dropped for consistency and safety of the API.
|
||||
Since this is a batching API, your implementation should NOT reject().
|
||||
|
||||
Please keep in mind that batching calls have to fit into an environment where they may
|
||||
be daisy chained onto other batching calls, and those results have to be amalgamated
|
||||
at the end of the chain into unique tables for each of the callers!
|
||||
]]
|
||||
function PerformFetch.Batch(items, keyMapper, fetchFunctor)
|
||||
assert(typeof(items) == "table")
|
||||
assert(typeof(keyMapper) == "function")
|
||||
assert(typeof(fetchFunctor) == "function")
|
||||
|
||||
return function(store)
|
||||
local itemsToFetch = {}
|
||||
local itemsToFetchKeyMap = {}
|
||||
local batchPromisesForItemsAlreadyBeingFetched = {}
|
||||
|
||||
-- Filter out items that do not need to be fetched
|
||||
for _, item in ipairs(items) do
|
||||
local fetchStatusKey = keyMapper(item)
|
||||
local fetchingStatus = PerformFetch.GetStatus(store:getState(), fetchStatusKey)
|
||||
local batchPromise = batchPromises[fetchStatusKey]
|
||||
|
||||
if batchPromise then
|
||||
assert(fetchingStatus == RetrievalStatus.Fetching)
|
||||
|
||||
batchPromisesForItemsAlreadyBeingFetched[fetchStatusKey] = batchPromise
|
||||
else
|
||||
assert(fetchingStatus ~= RetrievalStatus.Fetching)
|
||||
|
||||
table.insert(itemsToFetch, item)
|
||||
itemsToFetchKeyMap[item] = fetchStatusKey
|
||||
end
|
||||
end
|
||||
|
||||
local doResolve
|
||||
local batchFetchingPromise = Promise.new(function(resolve)
|
||||
doResolve = resolve
|
||||
end)
|
||||
|
||||
-- Call functor if there are items to fetch, otherwise short-circuit it
|
||||
-- We want to call it FIRST because we need to kick off async fetch before blocking
|
||||
-- on other responses.
|
||||
local functorPromise
|
||||
if #itemsToFetch > 0 then
|
||||
-- Place remaining items into fetching state and make entry in table before
|
||||
-- we kick off functor just in case it returns already-completed promise
|
||||
for _, fetchStatusKey in pairs(itemsToFetchKeyMap) do
|
||||
store:dispatch(UpdateFetchingStatus(fetchStatusKey, RetrievalStatus.Fetching))
|
||||
batchPromises[fetchStatusKey] = batchFetchingPromise
|
||||
end
|
||||
|
||||
functorPromise = fetchFunctor(store, itemsToFetch)
|
||||
assert(Promise.is(functorPromise))
|
||||
else
|
||||
functorPromise = Promise.resolve({})
|
||||
end
|
||||
|
||||
functorPromise:andThen(function(myResults)
|
||||
myResults = myResults or {} -- No resolve args = empty table for ease of use
|
||||
|
||||
return PromiseUtilities.Batch(batchPromisesForItemsAlreadyBeingFetched):andThen(function(batchResults)
|
||||
local filteredResults = {}
|
||||
for batchKey, batchResult in pairs(batchResults) do
|
||||
-- Extract only the result for the key we care about from the batch results
|
||||
local _, value = batchResult:unwrap()
|
||||
filteredResults[batchKey] = value[batchKey]
|
||||
end
|
||||
|
||||
return myResults, filteredResults
|
||||
end)
|
||||
end,
|
||||
function()
|
||||
assert(false, "PerformFetch fetchFunctor should never reject")
|
||||
end):andThen(function(myResults, batchResults)
|
||||
-- Iterate on requested items rather than on actual result set
|
||||
-- so that we are sure to check all our keys and ignore extra ones
|
||||
for _, fetchKey in pairs(itemsToFetchKeyMap) do
|
||||
local resultObj = myResults[fetchKey]
|
||||
if Result.is(resultObj) then
|
||||
batchResults[fetchKey] = resultObj
|
||||
else
|
||||
batchResults[fetchKey] = Result.error()
|
||||
end
|
||||
|
||||
-- Update fetching status in store from Result object status
|
||||
-- (The extra parens unwrap a multi-return value!)
|
||||
local itemStatus = (batchResults[fetchKey]:unwrap()) and RetrievalStatus.Done or RetrievalStatus.Failed
|
||||
store:dispatch(UpdateFetchingStatus(fetchKey, itemStatus))
|
||||
batchPromises[fetchKey] = nil
|
||||
end
|
||||
|
||||
return batchResults
|
||||
end):andThen(function(joinedResults)
|
||||
doResolve(joinedResults)
|
||||
end)
|
||||
|
||||
return batchFetchingPromise
|
||||
end
|
||||
end
|
||||
|
||||
return PerformFetch
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
return function()
|
||||
local PerformFetch = require(script.Parent.PerformFetch)
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Rodux = require(CorePackages.Rodux)
|
||||
local FetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
|
||||
|
||||
local function batchKeyMapper(item)
|
||||
return tostring(item) .. "_key"
|
||||
end
|
||||
|
||||
local TEST_ITEM_1 = "item1"
|
||||
local TEST_ITEM_2 = "item2"
|
||||
|
||||
local TEST_KEY_1 = batchKeyMapper(TEST_ITEM_1)
|
||||
local TEST_KEY_2 = batchKeyMapper(TEST_ITEM_2)
|
||||
|
||||
local function MockReducer(state, action)
|
||||
state = state or {}
|
||||
return {
|
||||
FetchingStatus = FetchingStatus(state.FetchingStatus, action),
|
||||
}
|
||||
end
|
||||
|
||||
local function makeResolver()
|
||||
local startResolve
|
||||
local startReject
|
||||
local testPromise = Promise.new(function(resolve, reject)
|
||||
startResolve = resolve
|
||||
startReject = reject
|
||||
end)
|
||||
|
||||
return {
|
||||
promise = testPromise,
|
||||
resolve = startResolve,
|
||||
reject = startReject
|
||||
}
|
||||
end
|
||||
|
||||
local function doDispatchSingle(store, key, functor)
|
||||
-- Wrap functor like a thunk would normally be
|
||||
local thunkFunc = function()
|
||||
return PerformFetch.Single(key, functor)
|
||||
end
|
||||
|
||||
return store:dispatch(thunkFunc())
|
||||
end
|
||||
|
||||
local function doDispatchBatch(store, items, functor)
|
||||
-- Wrap functor like a thunk would normally be
|
||||
local thunkFunc = function()
|
||||
return PerformFetch.Batch(items, batchKeyMapper, functor)
|
||||
end
|
||||
|
||||
return store:dispatch(thunkFunc())
|
||||
end
|
||||
|
||||
local function doBasicSingleTest(key)
|
||||
local resolver = makeResolver()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local thunkPromise = doDispatchSingle(store, key, function()
|
||||
return resolver.promise
|
||||
end)
|
||||
|
||||
return {
|
||||
store = store,
|
||||
resolver = resolver,
|
||||
promise = thunkPromise
|
||||
}
|
||||
end
|
||||
|
||||
local function doBasicBatchTest(keys)
|
||||
local resolver = makeResolver()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local thunkPromise = doDispatchBatch(store, keys, function()
|
||||
return resolver.promise
|
||||
end)
|
||||
|
||||
return {
|
||||
store = store,
|
||||
resolver = resolver,
|
||||
promise = thunkPromise
|
||||
}
|
||||
end
|
||||
|
||||
describe("PerformFetch.GetStatus", function()
|
||||
it("should return NotStarted for missing key", function()
|
||||
local state = { FetchingStatus = {} }
|
||||
local status = PerformFetch.GetStatus(state, TEST_KEY_1)
|
||||
|
||||
expect(status).to.equal(RetrievalStatus.NotStarted)
|
||||
end)
|
||||
|
||||
it("should return matching status for state in store", function()
|
||||
local statusesToTest = {
|
||||
RetrievalStatus.NotStarted,
|
||||
RetrievalStatus.Fetching,
|
||||
RetrievalStatus.Done,
|
||||
RetrievalStatus.Failed
|
||||
}
|
||||
|
||||
for _, testStatus in ipairs(statusesToTest) do
|
||||
local state = {
|
||||
FetchingStatus = {
|
||||
[TEST_KEY_1] = testStatus
|
||||
}
|
||||
}
|
||||
|
||||
expect(PerformFetch.GetStatus(state, TEST_KEY_1)).to.equal(testStatus)
|
||||
end
|
||||
|
||||
expect(#statusesToTest).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("PerformFetch.Single", function()
|
||||
it("should set fetching state in store when fetch begins", function()
|
||||
local bundle = doBasicSingleTest(TEST_KEY_1)
|
||||
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching)
|
||||
bundle.resolver.resolve() -- clear key from global fetchingPromiseMap or later tests get blocked
|
||||
end)
|
||||
|
||||
it("should pass store parameter to fetch functor", function()
|
||||
local originalStore = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local newStore
|
||||
doDispatchSingle(originalStore, TEST_KEY_1, function(store)
|
||||
newStore = store
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
expect(newStore ~= nil).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should set fetching state to done for sync resolve", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done)
|
||||
end)
|
||||
|
||||
it("should set fetching state to failed for sync reject", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.reject()
|
||||
end)
|
||||
|
||||
expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
it("should set fetching state to Done after async fetch resolves", function()
|
||||
local bundle = doBasicSingleTest(TEST_KEY_1)
|
||||
|
||||
bundle.resolver.resolve()
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done)
|
||||
end)
|
||||
|
||||
|
||||
it("should set fetching state to Failed after async fetch rejects", function()
|
||||
local bundle = doBasicSingleTest(TEST_KEY_1)
|
||||
|
||||
bundle.resolver.reject()
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
|
||||
it("should not mix fetching status of two separate keys", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_2, function()
|
||||
return Promise.reject()
|
||||
end)
|
||||
|
||||
expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done)
|
||||
expect(store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
it("should pass original promise args to daisy-chained promise upon resolve", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local testTable = { a = 1 }
|
||||
|
||||
local passedArg
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.resolve(testTable)
|
||||
end):andThen(function(results)
|
||||
passedArg = results
|
||||
end)
|
||||
|
||||
expect(passedArg).to.equal(testTable)
|
||||
end)
|
||||
|
||||
it("should pass original promise args to daisy-chained promise upon reject", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local testTable = { a = 1 }
|
||||
|
||||
local passedArg
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.reject(testTable)
|
||||
end):catch(function(results)
|
||||
passedArg = results
|
||||
end)
|
||||
|
||||
expect(passedArg).to.equal(testTable)
|
||||
end)
|
||||
|
||||
it("should not call second thunk instance for same key while request is ongoing", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local firstThunkExecuted = false
|
||||
local secondThunkExecuted = false
|
||||
|
||||
local firstThunkResolver = makeResolver()
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
firstThunkExecuted = true
|
||||
return firstThunkResolver.promise
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
secondThunkExecuted = true
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
expect(store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching)
|
||||
expect(firstThunkExecuted).to.equal(true)
|
||||
expect(secondThunkExecuted).to.equal(false)
|
||||
|
||||
firstThunkResolver.resolve()
|
||||
end)
|
||||
|
||||
it("should call both thunks when the first one is completed soon enough", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local firstThunkExecuted = false
|
||||
local secondThunkExecuted = false
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
firstThunkExecuted = true
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
secondThunkExecuted = true
|
||||
return Promise.resolve()
|
||||
end)
|
||||
|
||||
expect(firstThunkExecuted).to.equal(true)
|
||||
expect(secondThunkExecuted).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should resolve daisy-chained promises after thunk resolves", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local chainedPromiseExecuted = false
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.resolve()
|
||||
end):andThen(function()
|
||||
chainedPromiseExecuted = true
|
||||
end):catch(function()
|
||||
assert(false)
|
||||
end)
|
||||
|
||||
expect(chainedPromiseExecuted).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should reject daisy-chained promises after thunk rejects", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local chainedCatchExecuted = false
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.reject()
|
||||
end):andThen(function()
|
||||
assert(false)
|
||||
end):catch(function()
|
||||
chainedCatchExecuted = true
|
||||
end)
|
||||
|
||||
expect(chainedCatchExecuted).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should resolve daisy-chained promises on second thunk after first resolves", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local secondPromiseResolved = false
|
||||
|
||||
local startResolve
|
||||
local firstThunkPromise = Promise.new(function(resolve)
|
||||
startResolve = resolve
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return firstThunkPromise
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
assert(false)
|
||||
return Promise.reject()
|
||||
end):andThen(function()
|
||||
secondPromiseResolved = true
|
||||
end):catch(function()
|
||||
assert(false)
|
||||
end)
|
||||
|
||||
expect(secondPromiseResolved).to.equal(false)
|
||||
|
||||
startResolve()
|
||||
|
||||
expect(secondPromiseResolved).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should reject daisy-chained promises on second thunk after first thunk rejects", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local secondPromiseRejected = false
|
||||
|
||||
local startReject
|
||||
local firstThunkPromise = Promise.new(function(_, reject)
|
||||
startReject = reject
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return firstThunkPromise
|
||||
end)
|
||||
|
||||
doDispatchSingle(store, TEST_KEY_1, function()
|
||||
return Promise.new(function() end)
|
||||
end):andThen(function()
|
||||
assert(false)
|
||||
end):catch(function()
|
||||
secondPromiseRejected = true
|
||||
end)
|
||||
|
||||
expect(secondPromiseRejected).to.equal(false)
|
||||
|
||||
startReject()
|
||||
|
||||
expect(secondPromiseRejected).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("PerformFetch.Batch", function()
|
||||
it("should set fetching state in store for all batch items when fetching begins", function()
|
||||
local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 }
|
||||
local bundle = doBasicBatchTest(originalItemList)
|
||||
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Fetching)
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Fetching)
|
||||
|
||||
local results = {
|
||||
[TEST_KEY_1] = Result.new(true),
|
||||
[TEST_KEY_2] = Result.new(true),
|
||||
}
|
||||
|
||||
bundle.resolver.resolve(results) -- Cleanup to avoid test blockage
|
||||
end)
|
||||
|
||||
it("should set fetching state to matching status for all batch items when fetching completes successfully", function()
|
||||
local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 }
|
||||
local bundle = doBasicBatchTest(originalItemList)
|
||||
|
||||
local results = {
|
||||
[TEST_KEY_1] = Result.new(true),
|
||||
[TEST_KEY_2] = Result.new(false),
|
||||
}
|
||||
|
||||
bundle.resolver.resolve(results)
|
||||
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Done)
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
it("should fail items when they are not in the result list", function()
|
||||
local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 }
|
||||
local bundle = doBasicBatchTest(originalItemList)
|
||||
|
||||
bundle.resolver.resolve({})
|
||||
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_1]).to.equal(RetrievalStatus.Failed)
|
||||
expect(bundle.store:getState().FetchingStatus[TEST_KEY_2]).to.equal(RetrievalStatus.Failed)
|
||||
end)
|
||||
|
||||
it("should return a daisy chainable batch style promise that resolves with results", function()
|
||||
local originalItemList = { TEST_ITEM_1, TEST_ITEM_2 }
|
||||
local bundle = doBasicBatchTest(originalItemList)
|
||||
|
||||
local chainedResults = nil
|
||||
bundle.promise:andThen(function(promisedResults)
|
||||
chainedResults = promisedResults
|
||||
end)
|
||||
|
||||
local results = {
|
||||
[TEST_KEY_1] = Result.new(true, 42),
|
||||
[TEST_KEY_2] = Result.new(false, 29),
|
||||
}
|
||||
|
||||
bundle.resolver.resolve(results)
|
||||
|
||||
local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap()
|
||||
local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap()
|
||||
|
||||
expect(result1Status).to.equal(true)
|
||||
expect(result1Value).to.equal(42)
|
||||
|
||||
expect(result2Status).to.equal(false)
|
||||
expect(result2Value).to.equal(29)
|
||||
end)
|
||||
|
||||
it("should not call batch functor when there are no items to fetch", function()
|
||||
local store = Rodux.Store.new(MockReducer, { }, { Rodux.thunkMiddleware })
|
||||
local promise = doDispatchBatch(store, {}, function()
|
||||
assert(false, "Functor should not be called when there are no items to fetch")
|
||||
end)
|
||||
|
||||
local promiseResolved = false
|
||||
promise:andThen(function()
|
||||
promiseResolved = true
|
||||
end)
|
||||
|
||||
expect(promiseResolved).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should amalgamate results from multiple batch calls", function()
|
||||
local testItemList = { TEST_ITEM_1, TEST_ITEM_2 }
|
||||
|
||||
local bundle = doBasicBatchTest(testItemList)
|
||||
|
||||
local promise2 = doDispatchBatch(bundle.store, testItemList, function(_, itemsToFetch)
|
||||
assert(false, "second batch should not be called")
|
||||
return Promise.resolve({ })
|
||||
end)
|
||||
|
||||
local chainedResults = nil
|
||||
promise2:andThen(function(promisedResults)
|
||||
chainedResults = promisedResults
|
||||
end)
|
||||
|
||||
local results = {
|
||||
[TEST_KEY_1] = Result.new(true, 41),
|
||||
[TEST_KEY_2] = Result.new(false, 39),
|
||||
}
|
||||
|
||||
bundle.resolver.resolve(results)
|
||||
|
||||
local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap()
|
||||
local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap()
|
||||
|
||||
expect(result1Status).to.equal(true)
|
||||
expect(result1Value).to.equal(41)
|
||||
|
||||
expect(result2Status).to.equal(false)
|
||||
expect(result2Value).to.equal(39)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("PerformFetch with mixed Single/Batch", function()
|
||||
it("should include outstanding single results for matching batch keys", function()
|
||||
local singleBundle = doBasicSingleTest(TEST_KEY_1)
|
||||
|
||||
local batchItemCount = -1
|
||||
local batchPromise = doDispatchBatch(singleBundle.store, { TEST_ITEM_1, TEST_ITEM_2 },
|
||||
function(_, items)
|
||||
batchItemCount = #items
|
||||
return Promise.resolve({ [TEST_KEY_2] = Result.new(false, 35) })
|
||||
end)
|
||||
|
||||
singleBundle.resolver.resolve(49)
|
||||
|
||||
local chainedResults = nil
|
||||
batchPromise:andThen(function(results)
|
||||
chainedResults = results
|
||||
end)
|
||||
|
||||
local result1Status, result1Value = chainedResults[TEST_KEY_1]:unwrap()
|
||||
local result2Status, result2Value = chainedResults[TEST_KEY_2]:unwrap()
|
||||
|
||||
expect(result1Status).to.equal(true)
|
||||
expect(result1Value).to.equal(49)
|
||||
|
||||
expect(result2Status).to.equal(false)
|
||||
expect(result2Value).to.equal(35)
|
||||
|
||||
expect(batchItemCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should use batch result for duplicate single request", function()
|
||||
local batchBundle = doBasicBatchTest({ TEST_ITEM_1, TEST_ITEM_2 })
|
||||
|
||||
local singlePromise = doDispatchSingle(batchBundle.store, TEST_KEY_1, function()
|
||||
assert(false, "Single functor should not be called")
|
||||
return Promise.reject()
|
||||
end)
|
||||
|
||||
batchBundle.resolver.resolve({
|
||||
[TEST_KEY_1] = Result.new(true, 42),
|
||||
[TEST_KEY_2] = Result.new(true, 39)
|
||||
})
|
||||
|
||||
local chainedResult = nil
|
||||
singlePromise:andThen(function(result)
|
||||
chainedResult = result
|
||||
end)
|
||||
|
||||
expect(chainedResult).to.equal(42)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
return function(users)
|
||||
return function(store)
|
||||
local friendCountOffset = 0
|
||||
local updatedUsers = {}
|
||||
|
||||
for _, user in pairs(users) do
|
||||
local needsUpdate = false
|
||||
local userId = user.id
|
||||
local isFriend = user.isFriend
|
||||
local offset = 0
|
||||
|
||||
assert(typeof(isFriend) == "boolean")
|
||||
|
||||
local userInStore = store:getState().Users[userId]
|
||||
if userInStore then
|
||||
-- Mark user with needsUpdate if any of the field is different
|
||||
-- from the existing user information in Store.
|
||||
if not User.compare(userInStore, user) then
|
||||
needsUpdate = true
|
||||
if userInStore.isFriend ~= isFriend then
|
||||
offset = isFriend and 1 or -1
|
||||
end
|
||||
end
|
||||
else
|
||||
needsUpdate = true
|
||||
offset = isFriend and 1 or 0
|
||||
end
|
||||
|
||||
if needsUpdate then
|
||||
friendCountOffset = friendCountOffset + offset
|
||||
updatedUsers[userId] = user
|
||||
end
|
||||
end
|
||||
|
||||
if next(updatedUsers) then
|
||||
store:dispatch(AddUsers(updatedUsers))
|
||||
end
|
||||
|
||||
if friendCountOffset ~= 0 then
|
||||
local currentFriendCount = store:getState().FriendCount
|
||||
store:dispatch(SetFriendCount(currentFriendCount + friendCountOffset))
|
||||
end
|
||||
end
|
||||
end
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Rodux = require(CorePackages.Rodux)
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
|
||||
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
|
||||
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount)
|
||||
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
local function UsersReducerMonitor (state, action)
|
||||
state = state or {
|
||||
numberOfAddUsersCalled = 0,
|
||||
numberOfUsersPassedIn = 0,
|
||||
}
|
||||
|
||||
if action.type == AddUsers.name then
|
||||
state.numberOfAddUsersCalled = state.numberOfAddUsersCalled + 1
|
||||
state.numberOfUsersPassedIn = 0
|
||||
for _, _ in pairs(action.users) do
|
||||
state.numberOfUsersPassedIn = state.numberOfUsersPassedIn + 1
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local function FriendCountReducerMonitor (state, action)
|
||||
state = state or {
|
||||
numberOfSetFriendCountCalled = 0,
|
||||
}
|
||||
|
||||
if action.type == SetFriendCount.name then
|
||||
state.numberOfSetFriendCountCalled = state.numberOfSetFriendCountCalled + 1
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local function CustomReducer(state, action)
|
||||
state = state or {}
|
||||
|
||||
return {
|
||||
Users = Users(state.Users, action),
|
||||
UsersReducerMonitor = UsersReducerMonitor(state.UsersReducerMonitor, action),
|
||||
|
||||
FriendCount = FriendCount(state.FriendCount, action),
|
||||
FriendCountReducerMonitor = FriendCountReducerMonitor(state.FriendCountReducerMonitor, action),
|
||||
}
|
||||
end
|
||||
|
||||
local listOfUsers = {
|
||||
["1"] = User.fromData(1, "Hedonism Bot", true),
|
||||
["2"] = User.fromData(2, "Hypno Toad", true),
|
||||
["3"] = User.fromData(3, "John Zoidberg", false),
|
||||
["4"] = User.fromData(4, "Pazuzu", true),
|
||||
["5"] = User.fromData(5, "Ogden Wernstrom", true),
|
||||
["6"] = User.fromData(6, "Lrrr", true),
|
||||
}
|
||||
|
||||
it("should do nothing if empty list of users is provided", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
store:dispatch(UpdateUsers({ }))
|
||||
|
||||
local state = store:getState()
|
||||
|
||||
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(0)
|
||||
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should update only the number of users with modified data", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {
|
||||
Users = listOfUsers,
|
||||
}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
|
||||
local currentUsers = store:getState().Users
|
||||
local listOfUsersWithPotentialUpdates = {
|
||||
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- changed
|
||||
Immutable.Set(currentUsers["5"], "isFriend", false), -- changed
|
||||
Immutable.Set(currentUsers["6"], "isFriend", true), -- did not change
|
||||
}
|
||||
|
||||
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
|
||||
|
||||
local state = store:getState()
|
||||
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(1)
|
||||
expect(state.UsersReducerMonitor.numberOfUsersPassedIn).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should correctly update the number of friends", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
store:dispatch(UpdateUsers(listOfUsers))
|
||||
|
||||
local state = store:getState()
|
||||
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(1)
|
||||
expect(state.FriendCount).to.equal(5)
|
||||
|
||||
local currentUsers = store:getState().Users
|
||||
local listOfUsersWithPotentialUpdates = {
|
||||
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- friendship didn't change
|
||||
Immutable.Set(currentUsers["5"], "isFriend", false), -- friendship changed
|
||||
Immutable.Set(currentUsers["6"], "isFriend", false), -- friendship changed
|
||||
User.fromData(7, "Nibbler", true), -- new friend
|
||||
}
|
||||
|
||||
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
|
||||
|
||||
state = store:getState()
|
||||
expect(state.FriendCount).to.equal(4)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user