use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use omgbase_store::{IdMinter, RandomMinter, SequentialMinter, Store};
use omgbase_sync::WriterLockOptions;
use super::output::{CliError, js_string};
pub const SPEC_MINTER_ENV: &str = "OMGBASE_SPEC_MINTER";
pub const SPEC_CLOCK_ENV: &str = "OMGBASE_SPEC_CLOCK";
pub const WRITER_LOCK_TIMEOUT_ENV: &str = "OMGBASE_WRITER_LOCK_TIMEOUT_MS";
const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Seams {
pub sequential_minter: bool,
pub clock: Option<String>,
pub writer_lock_timeout_ms: Option<u64>,
}
impl Seams {
pub fn lock_options(&self) -> WriterLockOptions {
let defaults = WriterLockOptions::default();
match self.writer_lock_timeout_ms {
None => defaults,
Some(ms) => {
let timeout = Duration::from_millis(ms);
WriterLockOptions {
timeout,
poll: defaults.poll.min(timeout),
}
}
}
}
}
pub fn read_env_seams() -> Result<Seams, CliError> {
read_seams(
std::env::var(SPEC_MINTER_ENV).ok().as_deref(),
std::env::var(SPEC_CLOCK_ENV).ok().as_deref(),
std::env::var(WRITER_LOCK_TIMEOUT_ENV).ok().as_deref(),
)
}
pub fn read_seams(
minter: Option<&str>,
clock: Option<&str>,
lock_timeout: Option<&str>,
) -> Result<Seams, CliError> {
let sequential_minter = match minter.filter(|v| !v.is_empty()) {
None => false,
Some("sequential") => true,
Some(other) => {
return Err(CliError::usage_hint(
format!(
"{SPEC_MINTER_ENV}={}: the only value is \"sequential\" (spec/surface §7.1)",
js_string(other)
),
format!("unset it, or set {SPEC_MINTER_ENV}=sequential for a conformance run"),
));
}
};
let clock = match clock.filter(|v| !v.is_empty()) {
None => None,
Some(v) => Some(canonical_instant(v).ok_or_else(|| {
CliError::usage_hint(
format!(
"{SPEC_CLOCK_ENV}={}: not an RFC 3339 instant (spec/surface §7.1)",
js_string(v)
),
format!("e.g. {SPEC_CLOCK_ENV}=2026-09-27T00:00:00.000Z"),
)
})?),
};
let writer_lock_timeout_ms = match lock_timeout.filter(|v| !v.is_empty()) {
None => None,
Some(v) => Some(v.trim().parse::<u64>().map_err(|_| {
CliError::usage_hint(
format!(
"{WRITER_LOCK_TIMEOUT_ENV}={}: not a number of milliseconds",
js_string(v)
),
format!("unset it, or e.g. {WRITER_LOCK_TIMEOUT_ENV}=200 for a test run"),
)
})?),
};
Ok(Seams {
sequential_minter,
clock,
writer_lock_timeout_ms,
})
}
pub fn canonical_instant(v: &str) -> Option<String> {
use omgbase_store::time::{format_ms, parse_ms};
if let Ok(ms) = parse_ms(v) {
return Some(format_ms(ms));
}
let idx = v.len().checked_sub(6)?;
let (body, off) = v.split_at(idx);
let sign = match off.as_bytes().first()? {
b'+' => 1i64,
b'-' => -1i64,
_ => return None,
};
let (h, m) = off[1..].split_once(':')?;
let (h, m): (i64, i64) = (h.parse().ok()?, m.parse().ok()?);
if h > 23 || m > 59 {
return None;
}
let ms = parse_ms(&format!("{body}Z")).ok()?;
Some(format_ms(ms - sign * (h * 60 + m) * 60_000))
}
pub fn stamp(pinned: Option<&String>) -> String {
pinned.cloned().unwrap_or_else(omgbase_sync::now_ts)
}
#[derive(Clone)]
pub enum MinterSource {
Random,
Sequential(Arc<Mutex<SequentialMinter>>),
}
impl MinterSource {
pub fn from_seams(seams: &Seams) -> Self {
if seams.sequential_minter {
Self::Sequential(Arc::new(Mutex::new(SequentialMinter::new())))
} else {
Self::Random
}
}
pub fn minter(&self) -> Box<dyn IdMinter> {
match self {
Self::Random => Box::new(RandomMinter),
Self::Sequential(shared) => {
let shared = Arc::clone(shared);
Box::new(move |prefix: &str| {
shared
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.mint(prefix)
})
}
}
}
}
pub fn open_store(db: &Path, minters: &MinterSource) -> Result<Store, String> {
let store = Store::open_with_minter(db, minters.minter()).map_err(|e| e.to_string())?;
store
.conn()
.busy_timeout(BUSY_TIMEOUT)
.map_err(|e| format!("busy_timeout: {e}"))?;
Ok(store)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seams_parse_and_reject() {
assert_eq!(read_seams(None, None, None).unwrap(), Seams::default());
let s = read_seams(Some("sequential"), Some("2026-09-27T00:00:00Z"), None).unwrap();
assert!(s.sequential_minter);
assert_eq!(s.clock.as_deref(), Some("2026-09-27T00:00:00.000Z"));
assert_eq!(s.writer_lock_timeout_ms, None);
assert_eq!(s.lock_options(), WriterLockOptions::default());
let err = read_seams(Some("random"), None, None).unwrap_err();
assert_eq!(
err.message(),
"OMGBASE_SPEC_MINTER=\"random\": the only value is \"sequential\" (spec/surface §7.1)"
);
let err = read_seams(None, Some("yesterday"), None).unwrap_err();
assert_eq!(
err.message(),
"OMGBASE_SPEC_CLOCK=\"yesterday\": not an RFC 3339 instant (spec/surface §7.1)"
);
assert!(
!read_seams(Some(""), Some(""), Some(""))
.unwrap()
.sequential_minter
);
}
#[test]
fn writer_lock_timeout_seam() {
let s = read_seams(None, None, Some("150")).unwrap();
assert_eq!(s.writer_lock_timeout_ms, Some(150));
assert_eq!(
s.lock_options(),
WriterLockOptions {
timeout: Duration::from_millis(150),
poll: Duration::from_millis(25),
}
);
let s = read_seams(None, None, Some("10")).unwrap();
assert_eq!(s.lock_options().poll, Duration::from_millis(10));
assert_eq!(read_seams(None, None, Some("")).unwrap(), Seams::default());
let err = read_seams(None, None, Some("soon")).unwrap_err();
assert_eq!(
err.message(),
"OMGBASE_WRITER_LOCK_TIMEOUT_MS=\"soon\": not a number of milliseconds"
);
assert!(read_seams(None, None, Some("-1")).is_err());
}
#[test]
fn canonical_instant_accepts_offsets() {
assert_eq!(
canonical_instant("2026-09-27T02:30:00.5+02:30").as_deref(),
Some("2026-09-27T00:00:00.500Z")
);
assert_eq!(
canonical_instant("2026-09-26T23:00:00-01:00").as_deref(),
Some("2026-09-27T00:00:00.000Z")
);
assert_eq!(canonical_instant("2026-09-27"), None);
assert_eq!(canonical_instant("2026-09-27T00:00:00+25:00"), None);
}
#[test]
fn the_sequential_minter_is_shared_across_stores() {
let minters = MinterSource::from_seams(&Seams {
sequential_minter: true,
clock: None,
writer_lock_timeout_ms: None,
});
let mut a = minters.minter();
let mut b = minters.minter();
assert_eq!(a.mint("d"), "d_0");
assert_eq!(b.mint("d"), "d_1");
assert_eq!(a.mint("b"), "b_0");
let mut r = MinterSource::Random.minter();
assert!(r.mint("d").starts_with("d_"));
assert_eq!(
stamp(Some(&"2026-09-27T00:00:00.000Z".to_owned())),
"2026-09-27T00:00:00.000Z"
);
assert!(stamp(None).ends_with('Z'));
}
}