add gs
This commit is contained in:
+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
|
||||
Reference in New Issue
Block a user