Current section
Files
Jump to
Current section
Files
src/spoke/core/internal/session.gleam
//// The state of the session that persists over connections.
////
//// From the specs:
//// The Session state in the Client consists of:
//// * QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged.
//// * QoS 2 messages which have been received from the Server, but have not been completely acknowledged.
////
//// Also, to simplify things, we keep the packet id also.
import gleam/bool
import gleam/dict.{type Dict}
import gleam/list
import gleam/set.{type Set}
import spoke/core/session_state.{
type SessionState, type StorageUpdate, ClearPacketState, ClearSession,
ReceivedQoS2, UnackedQoS1, UnreceivedQoS2, UnreleasedQoS2, UpdatePacketState,
}
import spoke/packet
import spoke/packet/client/outgoing
pub opaque type Session {
Session(
clean_session: Bool,
packet_id: Int,
/// Published QoS 1 messages waiting for PubAck,
/// used for re-transmission of Publish.
unacked_qos1: Dict(Int, packet.MessageData),
/// Published QoS 2 messages waiting for PubRec,
/// used for re-transmission of Publish.
unreceived_qos2: Dict(Int, packet.MessageData),
/// Published QoS 2 message ids waiting for PubComp,
/// used for re-transmission of PubRel.
unreleased_qos2: Set(Int),
/// Received QoS 2 message ids waiting for PubRel from publisher,
/// used for de-duplication.
incomplete_qos2_in: Set(Int),
)
}
pub type PubAckResult {
PublishFinished(Session, List(StorageUpdate))
InvalidPubAckId
}
pub fn new(clean_session: Bool) -> Session {
Session(
clean_session:,
packet_id: 1,
unacked_qos1: dict.new(),
unreceived_qos2: dict.new(),
unreleased_qos2: set.new(),
incomplete_qos2_in: set.new(),
)
}
pub fn from_state(state: SessionState) -> Session {
let session = new(False)
let session = Session(..session, packet_id: state.packet_id)
let session = {
use session, packet_state <- list.fold(state.packet_states, session)
case packet_state {
#(id, UnackedQoS1(message)) ->
Session(
..session,
unacked_qos1: dict.insert(session.unacked_qos1, id, message),
)
#(id, UnreceivedQoS2(message)) ->
Session(
..session,
unreceived_qos2: dict.insert(session.unreceived_qos2, id, message),
)
#(id, ReceivedQoS2) ->
Session(
..session,
unreleased_qos2: set.insert(session.unreleased_qos2, id),
)
#(id, UnreleasedQoS2) ->
Session(
..session,
incomplete_qos2_in: set.insert(session.incomplete_qos2_in, id),
)
}
}
session
}
// [MQTT-3.1.2-6]
// If CleanSession is set to 1,
// the Client and Server MUST discard any previous Session and start a new one.
// This Session lasts as long as the Network Connection.
// State data associated with this Session MUST NOT be reused in any subsequent Session.
pub fn connect(
session: Session,
clean_session: Bool,
) -> #(Session, List(StorageUpdate)) {
// If the current session is not persisted, just return a new session
use <- bool.guard(
when: session.clean_session,
return: #(new(clean_session), []),
)
case clean_session {
// If not cleaning the session, continue current session
False -> #(session, [])
// Else clear persistent storage and start new session
True -> #(new(True), [ClearSession])
}
}
pub fn pending_publishes(session: Session) -> Int {
dict.size(session.unacked_qos1)
+ dict.size(session.unreceived_qos2)
+ set.size(session.unreleased_qos2)
}
pub fn reserve_packet_id(
session: Session,
) -> #(Session, Int, List(StorageUpdate)) {
let id = session.packet_id
let next_id = case id {
65_535 -> 1
id -> id + 1
}
use storage_updates <- store_if_persistent(session, [
session_state.StoreNextPacketId(next_id),
])
#(Session(..session, packet_id: next_id), id, storage_updates)
}
pub fn start_qos1_publish(
session: Session,
message: packet.MessageData,
) -> #(Session, outgoing.Packet, List(StorageUpdate)) {
let #(session, id, id_updates) = reserve_packet_id(session)
let unacked_qos1 = dict.insert(session.unacked_qos1, id, message)
let packet =
outgoing.Publish(packet.PublishDataQoS1(message, dup: False, packet_id: id))
use storage_updates <- store_if_persistent(session, [
UpdatePacketState(id, UnackedQoS1(message)),
..id_updates
])
#(Session(..session, unacked_qos1:), packet, storage_updates)
}
pub fn start_qos2_publish(
session: Session,
message: packet.MessageData,
) -> #(Session, outgoing.Packet, List(StorageUpdate)) {
let #(session, id, id_updates) = reserve_packet_id(session)
let unreceived_qos2 = dict.insert(session.unreceived_qos2, id, message)
let packet =
outgoing.Publish(packet.PublishDataQoS2(message, dup: False, packet_id: id))
use storage_updates <- store_if_persistent(session, [
UpdatePacketState(id, UnreceivedQoS2(message)),
..id_updates
])
#(Session(..session, unreceived_qos2:), packet, storage_updates)
}
pub fn handle_pubrec(
session: Session,
id: Int,
) -> #(Session, List(StorageUpdate)) {
let unreceived_qos2 = dict.delete(session.unreceived_qos2, id)
let unreleased_qos2 = set.insert(session.unreleased_qos2, id)
use storage_updates <- store_if_persistent(session, [
UpdatePacketState(id, ReceivedQoS2),
])
#(Session(..session, unreceived_qos2:, unreleased_qos2:), storage_updates)
}
pub fn handle_pubcomp(
session: Session,
id: Int,
) -> #(Session, List(StorageUpdate)) {
let unreleased_qos2 = set.delete(session.unreleased_qos2, id)
use storage_updates <- store_if_persistent(session, [ClearPacketState(id)])
#(Session(..session, unreleased_qos2:), storage_updates)
}
/// Marks a received packet id as unreleased.
/// The second element in the return value indicates whether we should
/// publish the message to the client or not.
pub fn start_qos2_receive(
session: Session,
packet_id: Int,
) -> #(Session, Bool, List(StorageUpdate)) {
case set.contains(session.incomplete_qos2_in, packet_id) {
True -> #(session, False, [])
False -> {
let incomplete_qos2_in = set.insert(session.incomplete_qos2_in, packet_id)
use storage_updates <- store_if_persistent(session, [
UpdatePacketState(packet_id, UnreleasedQoS2),
])
#(Session(..session, incomplete_qos2_in:), True, storage_updates)
}
}
}
pub fn handle_puback(session: Session, packet_id: Int) -> PubAckResult {
case dict.has_key(session.unacked_qos1, packet_id) {
True -> {
let unacked_qos1 = dict.delete(session.unacked_qos1, packet_id)
use storage_updates <- store_if_persistent(session, [
ClearPacketState(packet_id),
])
PublishFinished(Session(..session, unacked_qos1:), storage_updates)
}
False -> InvalidPubAckId
}
}
pub fn handle_pubrel(
session: Session,
packet_id: Int,
) -> #(Session, List(StorageUpdate)) {
let incomplete_qos2_in = set.delete(session.incomplete_qos2_in, packet_id)
use storage_updates <- store_if_persistent(session, [
ClearPacketState(packet_id),
])
#(Session(..session, incomplete_qos2_in:), storage_updates)
}
pub fn packets_to_send_after_connect(session: Session) -> List(outgoing.Packet) {
let packets = list.new()
// Add unacked Qos1 messages
let packets =
list.fold(dict.keys(session.unacked_qos1), packets, fn(packets, id) {
let assert Ok(message) = dict.get(session.unacked_qos1, id)
let packet =
outgoing.Publish(packet.PublishDataQoS1(
message,
dup: True,
packet_id: id,
))
[packet, ..packets]
})
// Add unreceived QoS2 messages
let packets =
list.fold(dict.keys(session.unreceived_qos2), packets, fn(packets, id) {
let assert Ok(message) = dict.get(session.unreceived_qos2, id)
let packet =
outgoing.Publish(packet.PublishDataQoS2(
message,
dup: True,
packet_id: id,
))
[packet, ..packets]
})
// Add unreleased QoS2 messages
let packets =
list.fold(set.to_list(session.unreleased_qos2), packets, fn(packets, id) {
[outgoing.PubRel(id), ..packets]
})
packets
}
fn store_if_persistent(
session: Session,
updates: List(StorageUpdate),
operation: fn(List(StorageUpdate)) -> a,
) -> a {
case session.clean_session {
False -> operation(updates)
True -> operation([])
}
}