Skip to content

Commit

Permalink
use generic types for actions (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
dig1t authored Aug 6, 2024
2 parents 5b905fd + 3de4000 commit a3854c8
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 68 deletions.
2 changes: 1 addition & 1 deletion src/Controller.luau
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ local store = Store.new()
local controllers: { [string]: ControllerType } = {}
local storeSubscriptions: { [number]: string } = {}

local _subscriptionId: string = store:subscribe(function(action: Types.Action)
local _subscriptionId: string = store:subscribe(function(action: Types.Action<any>)
for _, controllerId: string in pairs(storeSubscriptions) do
local controller: ControllerType = controllers[controllerId]

Expand Down
82 changes: 42 additions & 40 deletions src/Server.luau
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ Server.__index = Server
export type ServerType = typeof(setmetatable(
{} :: {
_reducers: {
[string]: (player: Player, payload: Types.ActionPayload?) -> any,
[string]: (player: Player, payload: Types.ActionPayload<any>?) -> any,
},
_privateReducers: {
[string]: (player: Player, payload: Types.ActionPayload?) -> any,
[string]: (player: Player, payload: Types.ActionPayload<any>?) -> any,
},
_services: {
[string]: any,
Expand Down Expand Up @@ -219,7 +219,7 @@ function Server.localCall(self: ServerType, actionType: string, ...): any
local serviceName: string = string.lower(string.split(actionType, "_")[1])
local service: Service.ServiceType = self._services[serviceName]

local success: boolean, res: Types.Action = pcall(
local success: boolean, res: Types.Action<any> = pcall(
self._reducers[actionType] or self._privateReducers[actionType],
service,
...
Expand All @@ -246,15 +246,15 @@ end
@within Server
@method _call
@param action Types.Action
@param action Types.Action<any>
@param player Player?
@return Types.Action?
@return Types.Action<any>?
]=]
function Server._call(
self: ServerType,
action: Types.Action,
action: Types.Action<any>,
player: Player?
): Types.Action?
): Types.Action<any>?
assert(action and typeof(action.type) == "string", "Action type must be a string")

if not (self._reducers[action.type] or self._privateReducers[action.type]) then
Expand Down Expand Up @@ -287,7 +287,7 @@ function Server._call(

service.name = serviceName

local success: boolean, res: Types.Action | string = pcall(
local success: boolean, res: Types.Action<any> | string = pcall(
self._reducers[action.type] or self._privateReducers[action.type],
service,
args[1], -- TODO: ??
Expand All @@ -309,7 +309,7 @@ function Server._call(
type = action.type,
err = typeof(res) == "string" and res or nil,
success = false,
} :: Types.Action
} :: Types.Action<any>
end

-- Add a success property to the action
Expand All @@ -322,7 +322,7 @@ function Server._call(
}
end

return res :: Types.Action
return res :: Types.Action<any>
end

--[=[
Expand Down Expand Up @@ -353,43 +353,45 @@ function Server.init(self: ServerType)
-- Hook listeners

-- store:dispatch() called from client
remotes.Client.OnServerEvent:Connect(function(player: Player, action: Types.Action)
if action.method and action.method == "get_result" then
return
end

action.player = player

local res: Types.Action? = self:_call(action, player)

if not res then
return
end
remotes.Client.OnServerEvent:Connect(
function(player: Player, action: Types.Action<any>)
if action.method and action.method == "get_result" then
return
end

-- If the action's method is "get" fire it back
-- to the sender
if action.method and action.method == "get" then
assert(
typeof(res) == "table",
action.type
.. " - Cannot return a "
.. typeof(res)
.. " type to a client."
)
action.player = player

res.uid = action.uid
local res: Types.Action<any>? = self:_call(action, player)

-- Change method to get result since
-- the server is now firing the result
-- to the client.
res.method = "get_result"
if not res then
return
end

remotes.Client:FireClient(player, res)
-- If the action's method is "get" fire it back
-- to the sender
if action.method and action.method == "get" then
assert(
typeof(res) == "table",
action.type
.. " - Cannot return a "
.. typeof(res)
.. " type to a client."
)

res.uid = action.uid

-- Change method to get result since
-- the server is now firing the result
-- to the client.
res.method = "get_result"

remotes.Client:FireClient(player, res)
end
end
end)
)

-- store:dispatch() called from server
remotes.Server.Event:Connect(function(action: Types.Action)
remotes.Server.Event:Connect(function(action: Types.Action<any>)
if action.method and action.method == "get_result" then
-- Ignore actions with get_result methods, these will
-- always be routed back into this listener
Expand Down
50 changes: 27 additions & 23 deletions src/Store.luau
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ local isServer: boolean = RunService:IsServer()
local Store = {}
Store.__index = Store

local _middleware: { (action: Types.Action) -> () } = {}
local _middleware: { (action: Types.Action<any>) -> () } = {}

export type StoreType = typeof(setmetatable(
{} :: {
_connections: { [number]: RBXScriptConnection },
_subscribers: { [Types.SubscriptionId]: (action: Types.Action) -> nil },
_subscribers: { [Types.SubscriptionId]: (action: Types.Action<any>) -> nil },
},
Store
))
Expand Down Expand Up @@ -61,9 +61,9 @@ export type StoreType = typeof(setmetatable(
@within Store
@method use
@param middleware (action: Types.Action) -> ()
@param middleware (action: Types.Action<any>) -> ()
]=]
function Store.use(middleware: (action: Types.Action) -> ())
function Store.use(middleware: (action: Types.Action<any>) -> ())
_middleware[#_middleware + 1] = middleware
end

Expand Down Expand Up @@ -139,12 +139,12 @@ end
@within Store
@method dispatch
@param target Player | { Player } | true
@param action Types.Action
@param action Types.Action<any>
]=]
function Store.dispatch(_self: StoreType, ...: any)
local args: { any } = { ... }

local action: Types.Action = #args == 1 and args[1] or args[2]
local action: Types.Action<any> = #args == 1 and args[1] or args[2]

assert(action ~= nil, "Dispatch requires an action")
assert(typeof(action) == "table", "Action must be a table")
Expand All @@ -164,7 +164,7 @@ function Store.dispatch(_self: StoreType, ...: any)
end

-- Handle middleware
for _, middleware: (action: Types.Action) -> () in _middleware do
for _, middleware: (action: Types.Action<any>) -> () in _middleware do
local success, err: any = pcall(middleware, action)

if not success then
Expand Down Expand Up @@ -238,10 +238,10 @@ end
@within Store
@method get
@param self StoreType
@param action Types.Action
@return Types.Action
@param action Types.Action<any>
@return Types.Action<any>
]=]
function Store.get(self: StoreType, action: Types.Action): Types.Action
function Store.get(self: StoreType, action: Types.Action<any>): Types.Action<any>
assert(typeof(action) == "table", "Action must be a table")
assert(action.type, "Action must have an action type")

Expand All @@ -251,13 +251,13 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action
action.method = "get"

local connections: { RBXScriptConnection } = {}
local res: Types.Action?
local res: Types.Action<any>?
local responseReceived: boolean?

local promise = Promise.new(function(resolve, _reject)
if isServer then
connections[#connections + 1] = remotes.Client.OnServerEvent:Connect(
function(player: Player, _action: Types.Action)
function(player: Player, _action: Types.Action<any>)
-- From client
_action.player = player

Expand All @@ -267,7 +267,7 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action
end
)
connections[#connections + 1] = remotes.Server.Event:Connect(
function(_action: Types.Action)
function(_action: Types.Action<any>)
-- From server
if _action.method == "get_result" and _action.uid == uid then
resolve(_action)
Expand All @@ -276,7 +276,7 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action
)
else
connections[#connections + 1] = remotes.Client.OnClientEvent:Connect(
function(_action: Types.Action)
function(_action: Types.Action<any>)
-- From server
if _action.method == "get_result" and _action.uid == uid then
resolve(_action)
Expand All @@ -287,14 +287,14 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action

self:dispatch(action)
end)
:andThen(function(result: Types.Action)
:andThen(function(result: Types.Action<any>)
res = result
responseReceived = true
end)
:catch(function(err: string)
res = {
err = err,
} :: Types.Action
} :: Types.Action<any>
end)
:finally(function()
for _, connection: RBXScriptConnection in pairs(connections) do
Expand All @@ -316,10 +316,14 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action
return res
or {
err = "Action failed to return a result. Timeout exceeded",
} :: Types.Action
} :: Types.Action<any>
end

function Store._callSubscribers(self: StoreType, action: Types.Action, safeCall: boolean?)
function Store._callSubscribers(
self: StoreType,
action: Types.Action<any>,
safeCall: boolean?
)
if not action.type or action.method == "get" or action.method == "get_result" then
-- Ignore get method actions, these
-- will be processed by store:get()
Expand All @@ -342,12 +346,12 @@ end
@within Store
@method subscribe
@param callback (action: Types.Action) -> ()
@param callback (action: Types.Action<any>) -> ()
@return Types.SubscriptionId
]=]
function Store.subscribe(
self: StoreType,
callback: (action: Types.Action) -> ()
callback: (action: Types.Action<any>) -> ()
): Types.SubscriptionId
assert(
typeof(callback) == "function",
Expand All @@ -362,7 +366,7 @@ function Store.subscribe(
if isServer then
-- From client
self._connections[#self._connections + 1] = remotes.Client.OnServerEvent:Connect(
function(player: Player, action: Types.Action)
function(player: Player, action: Types.Action<any>)
action.player = player

self:_callSubscribers(action, true)
Expand All @@ -371,14 +375,14 @@ function Store.subscribe(

-- From server
self._connections[#self._connections + 1] = remotes.Server.Event:Connect(
function(action: Types.Action)
function(action: Types.Action<any>)
self:_callSubscribers(action, true)
end
)
else
-- From client
self._connections[#self._connections + 1] = remotes.Client.OnClientEvent:Connect(
function(action: Types.Action)
function(action: Types.Action<any>)
self:_callSubscribers(action, false)
end
)
Expand Down
4 changes: 2 additions & 2 deletions src/Types.luau
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ export type SubscriptionId = string

export type ActionPayload<T> = T

export type Action = {
export type Action<T> = {
uid: string?,
method: string?,
player: Player?,
timeout: number?,
type: string,
payload: ActionPayload<any>,
payload: ActionPayload<T>,
success: boolean?,
err: string?,
}
Expand Down
2 changes: 1 addition & 1 deletion src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ red.Controller = Controller
export type SubscriptionId = string

export type ActionPayload<T> = Types.ActionPayload<T>
export type Action = Types.Action
export type Action<T> = Types.Action<T>

export type StateType = State.StateType

Expand Down
2 changes: 1 addition & 1 deletion wally.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dig1t/red"
version = "1.4.16"
version = "1.4.17"
license = "Apache-2.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
Expand Down

0 comments on commit a3854c8

Please sign in to comment.