import gleam/result import gleam/string import gleam/erlang/process.{type Subject} import gleam/otp/actor.{type Next} import smut/types.{ type SmutError, type TableHandle, type ValueHandle, NewExited, NewFailed, ValueHandle, } const default_timeout: Int = 5000 fn from_start_error(error: actor.StartError) -> SmutError { case error { actor.InitTimeout -> NewFailed("timeout") actor.InitFailed(reason) -> NewFailed(reason) actor.InitExited(reason) -> case reason { process.Normal -> NewExited("process exited normally") process.Killed -> NewExited("process was killed") process.Abnormal(reason) -> string.inspect(reason) |> NewExited() } } } @external(erlang, "erl_shim", "get_table") fn get_table() -> TableHandle @external(erlang, "erl_shim", "lookup") fn lookup_shim(table: TableHandle, key: Int) -> Result(a, Nil) @external(erlang, "erl_shim", "insert_new") fn insert_new_shim(table: TableHandle, val: a) -> Int @external(erlang, "erl_shim", "replace") fn replace_shim(table: TableHandle, key: Int, val: a) -> Nil /// Create a table in which to store shared, mutable state, and return a /// handle to it. /// /// This should be called as early as possible in your program, and the /// return value used to instantiate new state slots. /// /// ```gleam /// let global_table = smut.init() /// let counter = smut.new(0) /// smut.get(counter) |> echo /// // Ok(0) /// ``` pub fn init() -> TableHandle { get_table() } type Msg(state) { Replace(state) Update(fn(state) -> state) UpdateThenRetrieve(Subject(state), fn(state) -> state) RetrieveThenUpdate(Subject(state), fn(state) -> state) } /// A handle to an individual shared, mutable value of type `state`. pub opaque type State(state) { State(pipe: Subject(Msg(state)), handle: ValueHandle(state), timeout: Int) } /// Change the default retrieval timeout for this `state` handle from the /// default of 5000 ms to `ms` ms. pub fn with_timeout(state: State(state), ms: Int) -> State(state) { State(..state, timeout: ms) } fn lookup(handle: ValueHandle(a)) -> Result(a, Nil) { let ValueHandle(table, key) = handle lookup_shim(table, key) } fn replace(handle: ValueHandle(a), new_val: a) -> Nil { let ValueHandle(table, key) = handle replace_shim(table, key, new_val) } fn insert_new(table: TableHandle, val: a) -> ValueHandle(a) { let key = insert_new_shim(table, val) ValueHandle(table, key) } fn run_actor( handle: ValueHandle(state), message: Msg(state), ) -> Next(ValueHandle(state), Msg(state)) { case message { Replace(new_val) -> { replace(handle, new_val) actor.continue(handle) } Update(f) -> case lookup(handle) { Ok(val) -> { f(val) |> replace(handle, _) actor.continue(handle) } Error(_) -> actor.continue(handle) } UpdateThenRetrieve(return, f) -> case lookup(handle) { Ok(val) -> { let new_val = f(val) replace(handle, new_val) process.send(return, new_val) actor.continue(handle) } Error(_) -> actor.continue(handle) } RetrieveThenUpdate(return, f) -> case lookup(handle) { Ok(val) -> { process.send(return, val) f(val) |> replace(handle, _) actor.continue(handle) } Error(_) -> actor.continue(handle) } } } /// Create a new slot for a shared, mutable value of type `state`, and /// return a handle to it, through which reads, writes, and updates can /// be performed. pub fn new( table: TableHandle, initial_val: state, ) -> Result(State(state), SmutError) { let value_handle = insert_new(table, initial_val) use actor <- result.try( actor.new(value_handle) |> actor.on_message(run_actor) |> actor.start |> result.map_error(from_start_error), ) State(actor.data, value_handle, default_timeout) |> Ok() } /// Get the current value. pub fn get(state: State(state)) -> Result(state, Nil) { lookup(state.handle) } /// Set the value of the state to `new_val`. /// /// The majority of the time, you will probably want to use one of /// /// * `update()` /// * `update_and_retrieve()` /// * `retrieve_and_update()` /// /// instead. An operation like /// ```gleam /// let assert Ok(cur_val) = smut.get(state) /// let new_val = update_state(cur_val) /// smut.set(state, new_val) /// ``` /// is not guaranteed to be atomic. Another process could futz with the value /// of `state` between the call to `smut.get()` and `smut.set()`. The `update` /// family of functions ensure atomic updates. pub fn set(new_val: state, state: State(state)) -> Nil { process.send(state.pipe, Replace(new_val)) } /// Update the current value of `state` with the given update function. /// /// This operation is guaranteed to be atomic; that is, no other process /// can affect the value of `state` between its retrieval and replacement. pub fn update(with: fn(state) -> state, state: State(state)) -> Nil { process.send(state.pipe, Update(with)) } pub fn update_and_retrieve( with: fn(state) -> state, state: State(state), ) -> Result(state, Nil) { let return = process.new_subject() process.send(state.pipe, UpdateThenRetrieve(return, with)) process.receive(return, state.timeout) } pub fn retrieve_and_update( with: fn(state) -> state, state: State(state), ) -> Result(state, Nil) { let return = process.new_subject() process.send(state.pipe, RetrieveThenUpdate(return, with)) process.receive(return, state.timeout) }