Skip to main content

priv/ts/locks.ts

import { DOMException } from './dom-exception'

interface LockInfo {
  name: string
  mode: 'exclusive' | 'shared'
}

interface Lock {
  readonly name: string
  readonly mode: 'exclusive' | 'shared'
}

interface LockOptions {
  mode?: 'exclusive' | 'shared'
  ifAvailable?: boolean
  signal?: AbortSignal
}

interface LockManagerSnapshot {
  held: LockInfo[]
  pending: LockInfo[]
}

type LockGrantedCallback<T> = (lock: Lock | null) => T | Promise<T>

class LockManager {
  async request<T>(
    name: string,
    callbackOrOptions: LockOptions | LockGrantedCallback<T>,
    maybeCallback?: LockGrantedCallback<T>
  ): Promise<T> {
    let options: LockOptions = {}
    let callback: LockGrantedCallback<T>

    if (typeof callbackOrOptions === 'function') {
      callback = callbackOrOptions
    } else {
      options = callbackOrOptions
      if (!maybeCallback) throw new TypeError('callback is required')
      callback = maybeCallback
    }

    const mode = options.mode ?? 'exclusive'
    const ifAvailable = options.ifAvailable ?? false

    if (options.signal?.aborted) {
      throw new DOMException('The operation was aborted.', 'AbortError')
    }

    const result = await Beam.call('__locks_request', name, mode, ifAvailable)

    if (result === 'not_available') {
      return await callback(null)
    }

    if (result === 'holder_down') {
      throw new DOMException('Lock holder terminated', 'AbortError')
    }

    const lock: Lock = { name, mode }

    try {
      return await callback(lock)
    } finally {
      await Beam.call('__locks_release', name)
    }
  }

  async query(): Promise<LockManagerSnapshot> {
    return (await Beam.call('__locks_query')) as LockManagerSnapshot
  }
}

const lockManager = new LockManager()
const g = globalThis as Record<string, unknown>
g.navigator = g.navigator ?? {}
;(g.navigator as Record<string, unknown>).locks = lockManager