Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact 53a5e9b73b8431961349518c7cf73fb8aadb111105f04b11ac73905ab7c4b0fe:


import clock
import gleam/erlang/process.{type Subject}
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import gleam/result
import pace
import ql361api.{type Client, type LoginResult}
import ql361api/models.{type ApiError, ApiFailure, AuthExpired}

pub type Message {
  Ensure(reply: Subject(Result(#(Client, LoginResult), ApiError)))
  Invalidate(reply: Subject(Nil))
  Cached(reply: Subject(Bool))
  Reserve(reply: Subject(Nil))
}

type State {
  State(
    username: String,
    password: String,
    base_url: String,
    gap_ms: Int,
    last_request_ms: Int,
    session: Option(#(Client, LoginResult)),
  )
}

pub fn start(
  username: String,
  password: String,
  base_url: String,
  gap_ms: Int,
) -> Result(Subject(Message), actor.StartError) {
  let state =
    State(
      username: username,
      password: password,
      base_url: base_url,
      gap_ms: gap_ms,
      last_request_ms: 0,
      session: None,
    )
  case actor.new(state) |> actor.on_message(handle) |> actor.start() {
    Ok(started) -> Ok(started.data)
    Error(err) -> Error(err)
  }
}

pub fn ensure(
  session: Subject(Message),
) -> Result(#(Client, LoginResult), ApiError) {
  actor.call(session, 120_000, Ensure)
}

pub fn invalidate(session: Subject(Message)) -> Nil {
  actor.call(session, 5000, Invalidate)
}

pub fn cached(session: Subject(Message)) -> Bool {
  actor.call(session, 5000, Cached)
}

pub fn with_client(
  session: Subject(Message),
  f: fn(Client) -> Result(a, ApiError),
) -> Result(a, ApiError) {
  use pair <- result.try(ensure(session))
  reserve(session)
  case f(pair.0) {
    Error(AuthExpired(_)) -> {
      invalidate(session)
      use pair <- result.try(ensure(session))
      reserve(session)
      f(pair.0)
    }
    other -> other
  }
}

fn reserve(session: Subject(Message)) -> Nil {
  actor.call(session, 120_000, Reserve)
}

fn handle(state: State, message: Message) -> actor.Next(State, Message) {
  case message {
    Cached(reply) -> {
      let active = case state.session {
        Some(_) -> True
        None -> False
      }
      process.send(reply, active)
      actor.continue(state)
    }
    Invalidate(reply) -> {
      process.send(reply, Nil)
      actor.continue(State(..state, session: None))
    }
    Reserve(reply) -> {
      let state = take_slot(state)
      process.send(reply, Nil)
      actor.continue(state)
    }
    Ensure(reply) ->
      case state.session {
        Some(pair) -> {
          process.send(reply, Ok(pair))
          actor.continue(state)
        }
        None -> {
          let state = take_slot(state)
          case login(state) {
            Ok(pair) -> {
              process.send(reply, Ok(pair))
              actor.continue(State(..state, session: Some(pair)))
            }
            Error(err) -> {
              process.send(reply, Error(err))
              actor.continue(state)
            }
          }
        }
      }
  }
}

fn take_slot(state: State) -> State {
  let now = clock.monotonic_ms()
  let wait = pace.wait_ms(now, state.last_request_ms, state.gap_ms)
  case wait > 0 {
    True -> process.sleep(wait)
    False -> Nil
  }
  State(..state, last_request_ms: clock.monotonic_ms())
}

fn login(state: State) -> Result(#(Client, LoginResult), ApiError) {
  case state.username == "" || state.password == "" {
    True -> Error(ApiFailure("未配置 QL361_USERNAME 或 QL361_PASSWORD"))
    False ->
      case ql361api.login(username: state.username, password: state.password) {
        Error(err) -> Error(err)
        Ok(#(client, info)) -> {
          let client = case state.base_url {
            "" -> client
            url -> ql361api.with_base_url(client, url)
          }
          Ok(#(client, info))
        }
      }
  }
}


Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.