Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Middlewares #13

Merged
merged 20 commits into from
Mar 8, 2018
Merged

Middlewares #13

merged 20 commits into from
Mar 8, 2018

Conversation

AmaranthineCodices
Copy link
Contributor

@AmaranthineCodices AmaranthineCodices commented Mar 5, 2018

This fixes #5. I wanted to open this to allow code reviews.

TODO:

Copy-pasted from my issue comment:

The API now:

- function Store.new(reducer, initialState)
+ function Store.new(reducer, initialState, middlewares)

Middlewares are applied left-to-right order - the rightmost middleware is invoked first.

Middlewares look like this:

local function loggerMiddleware(next)
    return function(store, action)
        print("action dispatched:", action.type)
        next(action)
        print("new state:")
        printTable(store:getState())
    end
end

Using this looks like:

local store = Store.new(reducer, initialState, { loggerMiddleware })
store:dispatch({
    type = "someAction",
    ...
})
-- prints "action dispatched: someAction"
-- then the new state

@LPGhatguy
Copy link
Contributor

Looks like Travis caught this, but we should make sure that middlewares is an optional argument!

@coveralls
Copy link

coveralls commented Mar 5, 2018

Coverage Status

Coverage increased (+0.2%) to 99.296% when pulling b3724d7 on AmaranthineCodices:middlewares into 38ef143 on Roblox:master.

Copy link
Contributor

@LPGhatguy LPGhatguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Small feedback

local function loggerPlugin(next)
return function(store, action)
print("Action dispatched:", prettyPrint(action))
next(action)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should middleware be passing store as an argument as well as the next action? I figured that the self argument for the main reducer would get clobbered here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! You're right. Fixed now!

lib/init.lua Outdated

return {
Store = Store,
createReducer = createReducer,
combineReducers = combineReducers,
logger = loggerMiddleware,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we name logger loggerMiddleware in the public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured just calling it logger was okay, but I think loggerMiddleware is a better name in hindsight - it makes it clear that it's a middleware function, not something else.

end
})

expect(store.dispatch).to.never.equal(Store.dispatch)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to assert that this is the case -- if the middleware implementation didn't actually override the dispatch method, that would satisfy what we want still!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

next(store, action)
end
end
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should split out the middleware function (and maybe the reducer too?) to make this instantiation a little easier to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, should be good now.

@LPGhatguy
Copy link
Contributor

Ooh, I just noticed that this tanks our coverage.

Is there a way that we can change the loggerMiddleware to accept an arbitrary replacement print function without harming the outward API too much? I don't think we want to spam our tests with random stdout, but a no-op or instrumented print would be great.

@AmaranthineCodices
Copy link
Contributor Author

Done. The whole loggerMiddleware.outputFunction thing should never be documented; it's literally only there for testing purposes.

loggerPlugin.outputFunction("State changed to:"..prettyPrint(store:getState()))
end
end
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a not-actually-function thing from an API like this makes me kind of uncomfortable, but I'm not sure how else to structure this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither am I, without adding another layer to the logger >.<

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this be a table is still a blocker in my mind. I'd really like to validate the return values of middleware as they're applied to the store, otherwise it might be kind of tricky to debug them.

The only other API I can think of that would let you choose the output function would be:

local function loggerMiddleware(logFunction)
    logFunction = logFunction or print
    return function(_, next)
        return function(store, action)
            -- ...
        end
    end
end

And then the primary use of the middleware would be

Rodux.Store.new(reducer, nil, { Rodux.loggerMiddleware() })

I think that's mostly fine?

@AmaranthineCodices AmaranthineCodices changed the title WiP: Middlewares Middlewares Mar 5, 2018
@AmaranthineCodices
Copy link
Contributor Author

Barring a sudden insight in how to simplify loggerMiddleware, I think this is ready to merge.

@AmaranthineCodices
Copy link
Contributor Author

Any news on this @LPGhatguy? I'd like to get this landed and move on to #14 and #8 to pave the way for v1.0 to be released.

loggerPlugin.outputFunction("State changed to:"..prettyPrint(store:getState()))
end
end
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this be a table is still a blocker in my mind. I'd really like to validate the return values of middleware as they're applied to the store, otherwise it might be kind of tricky to debug them.

The only other API I can think of that would let you choose the output function would be:

local function loggerMiddleware(logFunction)
    logFunction = logFunction or print
    return function(_, next)
        return function(store, action)
            -- ...
        end
    end
end

And then the primary use of the middleware would be

Rodux.Store.new(reducer, nil, { Rodux.loggerMiddleware() })

I think that's mostly fine?

@AmaranthineCodices
Copy link
Contributor Author

That's a lot better than the hacky table approach; I've switched things over.

Copy link
Contributor

@LPGhatguy LPGhatguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks architecturally great! Last pieces are minor or just style nits.

type = "test"
})

expect(middlewareInvokeCount).to.equal(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this line got indented with spaces somehow!

return function(store, action)
outputFunction("Action dispatched: "..prettyPrint(action))
next(store, action)
outputFunction("State changed to:"..prettyPrint(store:getState()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we space out .. like we would other operators?

type = "test"
})

expect(outputCount >= 1).to.be.ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can check that the outputCount is exactly one now that we've defined that middleware aren't run on the initial action.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That test will fail, actually - loggerMiddleware calls outputFunction twice. I wrote this test so that it isn't super tied to the exact number of calls in the logger. I'm not sure if that's the best approach, but it was the one that seemed like the best option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, huh, I forgot about that.

Should we just make loggerMiddleware only get invoked once at the end of the middleware, with both the new state and action? We probably don't want to produce two separate log messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed now!

table.insert(outputBuffer, strKey)
table.insert(outputBuffer, " = ")

if type(value) == "table" then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use typeof here, since we normalized everywhere else!

"{\n"
}

for key, value in pairs(t) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to go through with this for the first version of the logger, but sorting the table keys so that we get stable output would be a good enhancement in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an ideal case, I'd actually like to switch the logger to displaying state deltas, so you only see what changed. That's something to revisit later, though.

Copy link
Contributor

@LPGhatguy LPGhatguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooray! Middleware!

@LPGhatguy LPGhatguy merged commit 400422a into Roblox:master Mar 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce middleware
3 participants