///|
/// Idempotent lifecycle handle for subscriptions, watches, and teardown
/// callbacks. Calling `dispose` more than once runs the callback at most once.
struct Disposable {
  dispose_handler : () -> Unit
}

///|
/// Builds a disposable handle from a teardown callback.
pub fn Disposable::from(dispose : () -> Unit) -> Disposable {
  let disposed = Ref(false)
  {
    dispose_handler: () => {
      if !disposed.val {
        disposed.val = true
        dispose()
      }
    },
  }
}

///|
/// Runs the disposable's teardown callback if it has not already run.
pub fn Disposable::dispose(self : Disposable) -> Unit {
  (self.dispose_handler)()
}

///|
priv struct ListenerCell[T] {
  id : Int32
  listener : (T) -> Unit
  active : Ref[Bool]
}

///|
/// Small typed event emitter for host-neutral subscription APIs. It stores
/// listeners in registration order and returns a disposable per listener.
struct Emitter[T] {
  listeners : Array[ListenerCell[T]]
  disposed : Ref[Bool]
  next_id : Ref[Int32]
}

///|
/// Creates an empty typed emitter.
pub fn[T] Emitter::Emitter() -> Emitter[T] {
  { listeners: [], disposed: Ref(false), next_id: Ref(0), }
}

///|
/// Registers a listener and returns a disposable for just that listener.
pub fn[T] Emitter::event(
  self : Emitter[T],
  listener : (T) -> Unit,
) -> Disposable {
  if self.disposed.val {
    return Disposable::from(() => ())
  }
  let id = self.next_id.val
  self.next_id.val = id + 1
  let active = Ref(true)
  self.listeners.push({ id, listener, active, })
  Disposable::from(() => {
    active.val = false
    self.listeners.retain(cell => cell.id != id)
  })
}

///|
/// Whether any active listener is registered, `Emitter.hasListeners`
/// (`event.ts`).
pub fn[T] Emitter::has_listeners(self : Emitter[T]) -> Bool {
  self.listeners.any(cell => cell.active.val)
}

///|
/// Notifies currently active listeners in registration order. Delivery
/// iterates a snapshot: a listener disposing itself (or a sibling) mid-fire
/// must not disturb the walk, and the `active` flag keeps listeners removed
/// mid-fire from running.
pub fn[T] Emitter::fire(self : Emitter[T], event : T) -> Unit {
  if self.disposed.val {
    return
  }
  let cells = self.listeners.copy()
  for cell in cells {
    if cell.active.val {
      (cell.listener)(event)
    }
  }
}

///|
/// Disposes every listener and prevents later registrations from firing.
pub fn[T] Emitter::dispose(self : Emitter[T]) -> Unit {
  if !self.disposed.val {
    self.disposed.val = true
    for cell in self.listeners {
      cell.active.val = false
    }
    self.listeners.clear()
  }
}

///|
/// Emitter that queues events fired in one tick and delivers them on a
/// scheduled flush, `MicrotaskEmitter` (`event.ts:1594-1620`). With a `merge`
/// function every event queued before the flush collapses into one merged
/// event.
///
/// Deviation: Monaco schedules the flush with a hardcoded `queueMicrotask`;
/// this package compiles for js *and* native, so the scheduler is injected at
/// construction. The default runs the flush inline, degenerating to the
/// synchronous `Emitter` semantics — the browser host passes the real
/// microtask scheduler (`viewer/services.mbt`).
struct MicrotaskEmitter[T] {
  inner : Emitter[T]
  mut queued_events : Array[T]
  merge_fn : ((Array[T]) -> T)?
  schedule : (() -> Unit) -> Unit
}

///|
/// Creates a queueing emitter; `merge` collapses one flush's events into one.
pub fn[T] MicrotaskEmitter::MicrotaskEmitter(
  merge? : (Array[T]) -> T,
  schedule~ : (() -> Unit) -> Unit = run => run(),
) -> MicrotaskEmitter[T] {
  { inner: Emitter(), queued_events: [], merge_fn: merge, schedule, }
}

///|
/// Registers a listener and returns a disposable for just that listener.
pub fn[T] MicrotaskEmitter::event(
  self : MicrotaskEmitter[T],
  listener : (T) -> Unit,
) -> Disposable {
  self.inner.event(listener)
}

///|
/// Queues the event; the queue's first event schedules the flush
/// (`MicrotaskEmitter.fire`). Events re-fired while the flush runs are
/// dropped, matching the source's `forEach` + reassignment.
pub fn[T] MicrotaskEmitter::fire(self : MicrotaskEmitter[T], event : T) -> Unit {
  if !self.inner.has_listeners() {
    return
  }
  self.queued_events.push(event)
  if self.queued_events.length() == 1 {
    (self.schedule)(() => {
      match self.merge_fn {
        Some(merge) => self.inner.fire(merge(self.queued_events))
        None => {
          let events = self.queued_events
          let count = events.length()
          for i in 0.. Unit {
  self.queued_events = []
  self.inner.dispose()
}