Skip to content

Commit

Permalink
process reducer cmds during effects reconciliation
Browse files Browse the repository at this point in the history
  • Loading branch information
eliknebel committed Nov 11, 2024
1 parent ffff26b commit a7aaff1
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 7 deletions.
36 changes: 29 additions & 7 deletions src/sprocket/internal/reducer.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub type ReducerMessage(model, msg) {
Shutdown
GetState(reply_with: Subject(model))
ReducerDispatch(msg: msg)
PushCommands(cmds: List(Cmd(msg)))
ProcessCommands
}

pub opaque type State(model, msg) {
Expand All @@ -32,6 +34,7 @@ pub opaque type State(model, msg) {
model: model,
update: UpdateFn(model, msg),
notify: fn(model) -> Nil,
pending_commands: List(Cmd(msg)),
)
}

Expand All @@ -44,7 +47,7 @@ pub fn init(
let self = process.new_subject()
let selector = process.selecting(process.new_selector(), self, identity)

actor.Ready(State(self, initial, update, notify), selector)
actor.Ready(State(self, initial, update, notify, []), selector)
}
}

Expand All @@ -64,15 +67,15 @@ pub fn handle_message(
}

ReducerDispatch(msg) -> {
let State(self:, model:, update:, notify:) = state
let State(self:, model:, update:, notify:, ..) = state

// This is the main logic for updating the reducer's state. The reducer function will
// return the updated model and a list of zero or more commands to execute. The commands
// are functions that will be called with the dispatcher function which may trigger
// additional messages to the reducer.
let #(updated_model, cmds) = update(model, msg)

async_process_commands(cmds, dispatch(self, _))
process.send(self, PushCommands(cmds))

// Notiify the parent component that the state has been updated. In the case
// of a view component, this will trigger a re-render. The re-render will call
Expand All @@ -83,6 +86,25 @@ pub fn handle_message(

actor.continue(State(..state, model: updated_model))
}

PushCommands(cmds) -> {
let State(pending_commands:, ..) = state

let pending_commands = list.append(pending_commands, cmds)

actor.continue(State(..state, pending_commands:))
}

ProcessCommands -> {
let State(self:, pending_commands:, ..) = state

pending_commands
|> list.each(fn(cmd) {
process.start(fn() { cmd(dispatch(self, _)) }, False)
})

actor.continue(State(..state, pending_commands: []))
}
}
}

Expand All @@ -108,7 +130,7 @@ pub fn start(
)),
)

async_process_commands(cmds, dispatch(self, _))
process.send(self, PushCommands(cmds))

self
}
Expand All @@ -132,7 +154,7 @@ pub fn dispatch(subject: Subject(ReducerMessage(model, msg)), msg: msg) -> Nil {
actor.send(subject, ReducerDispatch(msg))
}

fn async_process_commands(cmds, dispatcher) -> Nil {
cmds
|> list.each(fn(cmd) { process.start(fn() { cmd(dispatcher) }, False) })
/// Processes any pending commands in the reducer actor.
pub fn process_commands(subject) -> Nil {
process.send(subject, ProcessCommands)
}
10 changes: 10 additions & 0 deletions src/sprocket/runtime.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import sprocket/internal/reconcile.{
ReconciledFragment, ReconciledResult,
}
import sprocket/internal/reconcilers/recursive
import sprocket/internal/reducer
import sprocket/internal/utils/ordered_map.{
type KeyedItem, type OrderedMapIter, KeyedItem,
}
import sprocket/internal/utils/timer
import sprocket/internal/utils/unique.{type Unique}
import sprocket/internal/utils/unsafe_coerce

pub type Runtime =
Subject(Message)
Expand Down Expand Up @@ -514,6 +516,14 @@ fn run_effects(rendered: ReconciledElement) -> ReconciledElement {
Effect(id, effect_fn, deps, Some(result))
}

Reducer(id, reducer_actor, cleanup) -> {
reducer_actor
|> unsafe_coerce.unsafe_coerce()
|> reducer.process_commands()

Reducer(id, reducer_actor, cleanup)
}

other -> other
}
})
Expand Down
24 changes: 24 additions & 0 deletions test/sprocket/hooks_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,18 @@ pub fn reducer_should_run_cmds_test() {

let spkt = render_event(spkt, ClickEvent, "random")

// reducer commands are run asynchonously so we need to wait for the command to be processed
// before we can test the initial state
test_helpers.wait_until(
fn() {
let #(_spkt, rendered) = render_html(spkt)

rendered
|> string.starts_with("current count is: 42")
},
1000,
)

let #(_spkt, rendered) = render_html(spkt)

let assert True =
Expand Down Expand Up @@ -375,6 +387,18 @@ pub fn reducer_should_initialize_with_cmds_test() {

let spkt = connect(view)

// reducer commands are run asynchonously so we need to wait for the command to be processed
// before we can test the initial state
test_helpers.wait_until(
fn() {
let #(_spkt, rendered) = render_html(spkt)

rendered
|> string.starts_with("current count is: 42")
},
1000,
)

let #(spkt, rendered) = render_html(spkt)

let assert True =
Expand Down
44 changes: 44 additions & 0 deletions test/sprocket/internal/reducer_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import sprocket/internal/reducer

type Model =
Int

type Msg {
Increment
Set(Int)
Reset
}

fn init(value) -> #(Model, List(reducer.Cmd(Msg))) {
#(value, [])
}

fn update(model, msg) -> #(Model, List(reducer.Cmd(Msg))) {
case msg {
Increment -> #(model + 1, [])
Set(value) -> #(value, [])
Reset -> #(0, [])
}
}

pub fn reducer_should_dispatch() {
let assert Ok(reducer_actor) = reducer.start(init(0), update, fn(_) { Nil })

reducer.dispatch(reducer_actor, Increment)

let state = reducer.get_state(reducer_actor)

let assert True = state == 1

reducer.dispatch(reducer_actor, Increment)

let state = reducer.get_state(reducer_actor)

let assert True = state == 2

reducer.dispatch(reducer_actor, Reset)

let state = reducer.get_state(reducer_actor)

let assert True = state == 0
}

0 comments on commit a7aaff1

Please sign in to comment.