This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,5 @@
{
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
{
"language": {
"mode": "nonstrict"
}
}
@@ -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
@@ -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
@@ -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